1

Consider the following code:

#include <stdio.h>
#include <unistd.h>

void foo(char * const arg[]) {
    printf("success\n");
}

int main() {
    char myargs[2][64] = { "/bin/ls", NULL };

    foo(myargs);
    execv(myargs[0], myargs);
    return 0;
}

Both foo and execv require char * const * argument, but while my foo works (I get success in the output) the system call execv fails.

I would like to know why. Does this have something to do with the implementation of execv?

Also, assuming I have a char** variable - how can I send it to execv?

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
Z E Nir
  • 332
  • 1
  • 2
  • 15
  • Hang on. If this compiled and you got output the problem has little to do with implicit conversions. Are you asking about a compiler error? Or a rumtime error? – StoryTeller - Unslander Monica Dec 12 '18 at 14:09
  • 3
    `const` is the least of your problems. A two-dimensional array is fundamentally different from an array of pointers. –  Dec 12 '18 at 14:13
  • 1
    Compile with `-Wall` and pay attention to the warnings. There are a few problems with this code. The call to `foo` is also incorrect. – interjay Dec 12 '18 at 14:30
  • Do not post code with line numbers. It substantially reduces the value of posting code at all. – John Bollinger Dec 12 '18 at 14:36
  • Thanks for your advices. few points: I talked about runtime error. compiling with `-Wall` didn't change the results (it compiled with the same warnings). I didnt knew there is so much difference between 2D array to array of pointers, as long as I know its just a different way to allocate the memory... and I will stop post code with line numbers... :) – Z E Nir Dec 12 '18 at 14:45

2 Answers2

3

A two-dimensional array looks like this:

char myargs[2][16];

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

I reduced the size from 64 to 16 to keep the diagram from being annoyingly big.

With an initializer, it can look like this:

char myargs[2][16] = { "/bin/ls", "" }

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| /| b| i| n| /| l| s|\0|  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|\0|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

Notice I didn't try to put a null pointer in the second row. It doesn't make sense to do that, since that's an array of chars. There's no place in it for a pointer.

The rows are contiguous in memory, so if you look at a lower level, it's actually more like this:

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| /| b| i| n| /| l| s|\0|  |  |  |  |  |  |  |  |\0|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

When you pass myargs to a function, the famous "array decay" produces a pointer. That looks like this:

void foo(char (*arg)[16]);
...
char myargs[2][16] = { "/bin/ls", "" }
foo(myargs);

+-----------+    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|  POINTER==|===>| /| b| i| n| /| l| s|\0|  |  |  |  |  |  |  |  |\0|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+-----------+    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The pointer is arg contains a value which locates the beginning of the array. Notice there is no pointer pointing to the second row. If foo wants to find the value in the second row, it needs to know how big the rows are so it can break down this:

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| /| b| i| n| /| l| s|\0|  |  |  |  |  |  |  |  |\0|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

into this:

+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
| /| b| i| n| /| l| s|\0|  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
|\0|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |
+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

That's why arg must be char (*arg)[16] and not char **arg or the equivalent char *arg[].

The exec family of functions doesn't work with this data layout. It wants this:

+-----------+    +-----------+-----------+
|  POINTER==|===>|  POINTER  |    NULL   |
+-----------+    +-----|-----+-----------+
                       |
/----------------------/
|
|
|    +--+--+--+--+--+--+--+--+
\--->| /| b| i| n| /| l| s|\0|
     +--+--+--+--+--+--+--+--+

And when you want to add more arguments, it wants this:

+-----------+    +-----------+-----------+-   -+-----------+
|  POINTER==|===>|  POINTER  |  POINTER  | ... |    NULL   |
+-----------+    +-----|-----+-----|-----+-   -+-----------+
                       |           |
/----------------------/           |
|                                  |
| /--------------------------------/
| |
| |
| |  +--+--+--+--+--+--+--+--+
\-+->| /| b| i| n| /| l| s|\0|
  |  +--+--+--+--+--+--+--+--+
  |
  |  +--+--+--+--+--+--+
  \->| /| h| o| m| e|\0|
     +--+--+--+--+--+--+

If you compare this to the two-dimensional array diagram, hopefully you can understand why this can't be an implicit conversion. It actually involves moving stuff around in memory.

2

Both foo and execv require char * const * argument,

Yes.

but while my foo works (I get success in the output) the system call execv fails.

Getting the output you expect does not prove that your code is correct. The call exhibits undefined behavior because its argument does not match the parameter type, but it is plausible that that has little practical effect because the implementation of foo() does not use the parameter in any way. More generally, your code could, in principle, exhibit absolutely any behavior at all, because that's what "undefined" means.

I would like to know why. Does this have something to do with the implementation of execv?

From the standard's perspective, both calls exhibit equally undefined behavior. As a practical matter, however, we know that execv does use its arguments, so it would be much more surprising for that call to produce the behavior you expected than it is for the call to foo to produce the behavior you expected.

The main problem is that 2D arrays are arrays of arrays, and arrays are not pointers. Thus, your 2D array myargs does not at all have the correct type for an argument to either function.

Also, assuming I have a char** variable - how can I send it to execv?

You do not have such a variable in your code, but if you did have, you could cast it to the appropriate type:

char *some_args[] = { "/bin/ls", NULL };
execv((char * const *) some_args);

In practice, most compilers would probably accept it if you omitted the cast, too, although the standard does require it. Best would be to declare a variable that has the correct type in the first place:

char * const correct_args[] = { "/bin/ls", NULL };
execv(correct_args);

Note also that although arrays are not pointers, they are converted to pointers in most contexts -- which I use in the example code -- but only the top level. An array of arrays thus "decays" to a pointer to an array, not a pointer to a pointer.

John Bollinger
  • 160,171
  • 8
  • 81
  • 157
  • Thanks for the great exlpanation and for the **solutions you suggest**. I can try the explicit casting, but I decided to make some major changes in the code I'm working on that bring up my question in the first place. So instead of the `char[][]` I will use `char * const *` as you said... – Z E Nir Dec 12 '18 at 18:41