C offers no mechanism to pass the dimensions of variable-sized multidimensional arrays around. You have to implement it yourself, i.e. you have to come up with your own type that means int array[10][20]. For example, in the (untested) code below, you can pass around an int_array *, and use int_at to get the value. You can also inspect the dimensions within int_array, etc. The code using the int_array * doesn't even need to know how many dimensions it has got.
vint_ptr_at is doing, pretty much, what the compiler-generated code would do to compute the address, although that code can be optimized better than the generic version. Normally, if you were to iterate, you could use the int* you got from int_ptr_at, and keep incrementing it.
#include <assert.h>
#include <stdarg.h>
#include <stdbool.h>
#include <stdint.h>
typedef struct {
  int *data;
  int num_dims;
  bool owns_data;
  struct {
    size_t count, product;
  } dims[1];
} int_array;
void free_int_array(int_array *arr) {
  if (arr && arr->owns_data) free(arr->data);
#ifdef DEBUG
  arr->data = NULL;
#endif
  free(arr);
}
static bool int_array_setup_dims(int_array *arr, va_list args) {
  size_t n = 1;
  int const num_dims = arr->num_dims;
  for (int i = 0; i < num_dims; i++) {
    int dim = va_arg(args, int);
    arr->dims[i].count = dim;
    arr->dims[num_dims - 1 - i].product = n;
    if (n > SIZE_MAX / dim) return false;
    n *= dim;
  }
  return true;
}
int_array *vnew_int_array_on(void *data, int num_dims, va_list args) {
  int_array *arr = malloc(sizeof(int_array) + (num_dims-1)*sizeof(int));
  if (!arr) goto fail;
  arr->num_dims = num_dims;
  if (!int_array_setup_dims(arr, args)) goto fail;
  arr->data = data ? data : malloc(n * sizeof(int));
  arr->num_dims = num_dims;
  arr->owns_data = !data;
  if (!arr->data) goto fail;
  return arr;
fail:
  free(arr);
  return NULL;
}
int_array *new_int_array(int num_dims, ...) {
  va_list args;
  va_start(args, num_dims);
  int_array *arr = vnew_int_array_on(NULL, num_dims, args);
  va_end(args);
  return arr;
}
int_array *new_int_array_on(void *data, int num_dims, ...) {
  va_list args;
  va_start(args, num_dims);
  int_array *arr = vnew_int_array_in(data, num_dims, args);
  va_end(args);
  return arr;
}
int *vint_ptr_at(const int_array *arr, va_list args) {
  size_t index = 0, n = 1;
  int const num_dims = arr->num_dims;
  for (int i = 0; i < num_dims; i++) {
    int j = va_arg(args, int);
    index += j * arr->dims[i].product;
  }
  return arr->data + index;
}
int *int_ptr_at(const int_array *arr, ...) {
  va_list args;
  va_start(args, arr);
  int *ptr = vint_ptr_at(arr, args);
  va_end(args);
  return ptr;
}  
int int_at(const int_array *arr, ...) {
  va_list args;
  va_start(args, arr);
  int *ptr = vint_ptr_at((int_array*)arr, args);
  va_end(args);
  return *ptr;
}
size_t *indices_for(const int_array *arr) {
  if (!arr) return NULL;
  size_t const size = sizeof(size_t) * arr->num_dims;
  size_t *indices = malloc(size);
  if (indices) memset(indices, 0, size);
  return indices;
}
bool next_index(const int_array *arr, size_t *indices) {
  for (int i = arr->num_dims - 1; i >= 0; i--) {
    if (indices[i] < arr->dims[i].count) {
      indices[i] ++;
      return true;
    }
    indices[i] = 0;
  }
  return false;
}
int main() {
  int data[10][15];
  int_array *arr = new_int_array_on(data, 2, 10, 15);
  assert(arr->dims[0].dim == 10);
  assert(arr->dims[1].dim == 15);
  data[2][3] = 40;
  data[4][5] = 50;
  data[5][6] = 100;
  assert(int_at(arr, 2, 3) == 40);
  assert(int_at(arr, 4, 5) == 50);
  assert(int_at(arr, 5, 6) == 100);
  free_int_array(arr);
}
#include <stdio.h>
void iteration_example(const int_array *arr) {
  size_t *indices = indices_for(arr);
  if (indices) {
    int *data = arr->data;
    do {
      printf("arr");
      for (int i = 0; i < arr->num_dims; ++i)
        printf("[%zd]", indices[i]);
      printf(" = %d\n", *data++);
    } while (next_index(arr, indices));
  }
  free(indices);
}