January 02, 2020 Ah, welcome! I see you know C. Good! Help me out, will you? Here's a tiny little program: int main() { if (setenv("FOO", "bar", 1) < 0) { perror("Unable to setenv"); return 1; } execl("/tmp/prog", (char *) 0); perror("Unable to execl"); return 1; } This is going to fail, right? Tell me why. I thought I knew why, but struggled to explain it. So I started to dig in a bit, and it turns out that the answer is really not quite as obvious as " execl(3) requires at least three arguments". Although that is true: if you try to compile it the way you should (i.e., with -Wall -Werror ), it will give you an error: $ cc -Wall -Werror fail.c fail.c: In function 'main': fail.c:10:2: error: not enough variable arguments to fit a sentinel [-Werror=format=] execl("/tmp/prog", (char *) 0); ^ cc1: all warnings being treated as errors $ Lesson 1: Always enable warnings, treat warnings as errors. But let's pretend we're a student who hasn't yet learrned to always use -Wall -Werror (and some of the many other compiler warnings). Then we're bound to see: $ cat /tmp/prog #! /bin/sh env $ cc fail.c $ ./a.out Unable to execl: Bad address $ Makes sense, right? The function prototype and description for execl(3) is: int execl(const char *path, const char *arg, ...); The const char *arg and subsequent ellipses in the execl(), execlp(), execlpe(), and execle() functions can be thought of as arg0, arg1, ..., argn. Together they describe a list of one or more pointers to NUL- terminated strings that represent the argument list available to the executed program. The first argument, by convention, should point to the file name associated with the file being executed. The list of arguments must be terminated by a NULL pointer. The exect(), execv(), execvp(), and execvpe() functions provide an array of pointers to NUL-terminated strings that represent the argument list available to the new program. The first argument, by convention, should point to the file name associated with the file being executed. The array of pointers must be terminated by a NULL pointer. Passing NUL as the second argument (which will become arg0 ), is bound to mess things up, and execution fails. Ok, cool beans. But suppose you instead tried this program: int main() { execl("/tmp/prog", (char *) 0); perror("Unable to execl"); return 1; } Not much different there, we just ditched the call to setenv(3) . And so: $ cat /tmp/prog #! /bin/sh env $ cc fail.c $ env -i ./a.out PWD=/home/jschauma $ Hmm, ok, so setenv(3) seems to mess things up. But that call itself didn't fail - let's check, to make sure: $ cat fail.c #include <stdio.h> #include <stdlib.h> #include <unistd.h> int main() { if (setenv("FOO", "bar", 1) < 0) { perror("Unable to setenv"); return 1; } printf("We set FOO to '%s'.

", getenv("FOO")); execl("/tmp/prog", (char *) 0); perror("Unable to execl"); return 1; } $ cc fail.c $ env -i ./a.out We set FOO to 'bar'. FOO=bar PWD=/home/jschauma $ Uhm... ok. Adding a printf(3) statement "fixes" the problem. Cool story. That doesn't help us figure out what went wrong at all. Lesson 2: printf debugging has its limits and may at times hide the bug you're trying to find or fix. What do we do when printf debugging fails us? Well, I suppose we can begrudgingly pull out our debugger, gdb(1) . Let's remove the printf statement above and return to our initial failing version of the program. But wait... we don't have a libc with debugging symbols, so we can't actually step into execl(3) ; our debugging session is not going to be very fruitful: $ cc -g fail.c $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) break main Breakpoint 1 at 0x400904: file fail.c, line 6. (gdb) run Starting program: /home/jschauma/a.out Breakpoint 1, main () at fail.c:6 6 if (setenv("FOO", "bar", 1) < 0) { (gdb) s 10 execl("/tmp/prog", (char *) 0); (gdb) 11 perror("Unable to execl"); (gdb) Unable to execl: Bad address 12 return 1; (gdb) 13 } How do we figure out what's going on in execl(3) ? Wait, this is an open source OS -- let's use the source! Specifically, let's look at /usr/src/lib/libc/gen/execl.c: int execl(const char *name, const char *arg, ...) { int r; va_list ap; char **argv; int i; va_start(ap, arg); for (i = 2; va_arg(ap, char *) != NULL; i++) continue; va_end(ap); if ((argv = alloca(i * sizeof (char *))) == NULL) { errno = ENOMEM; return -1; } va_start(ap, arg); argv[0] = __UNCONST(arg); for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++) continue; va_end(ap); r = execve(name, argv, environ); return r; } We'll copy this definition into our source file as " myexecl " and then debug that. So that we don't have to recompile the program to trigger the different scenarios we're debugging, we extend the code minimally: extern char **environ; int myexecl(const char *name, const char *arg, ...); int main(int argc, char **argv) { if (argc > 1) { if (setenv("FOO", "bar", 1) < 0) { perror("Unable to setenv"); return 1; } if (argc > 2) { printf("We set FOO to '%s'.

", getenv("FOO")); } } myexecl("/tmp/prog", (char *) 0); perror("Unable to execl"); return 1; } int myexecl(const char *name, const char *arg, ...) { int r; va_list ap; char **argv; int i; va_start(ap, arg); for (i = 2; va_arg(ap, char *) != NULL; i++) { continue; } va_end(ap); if ((argv = alloca(i * sizeof (char *))) == NULL) { errno = ENOMEM; return -1; } va_start(ap, arg); argv[0] = __UNCONST(arg); for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++) { continue; } va_end(ap); r = execve(name, argv, environ); return r; } With this in place, we can now step into " myexecl " and see what's happening there: execl(3) iterates over the variable number of arguments starting after ' arg ' to determine how many there are (stored in i ), then allocates a pointer argv on the stack (via alloca(3) ) that's i elements large; then it populates that argv with the given arguments before calling execve(2) , passing the newly constructed argv as well as the extern char **environ pointer. It seems that the failure we observed depends on the processing of the variadic arguments to build the argv ; the last argument in this list MUST be " (char *)0 ", as noted in the manual page, but isn't passing NUL as arg sufficient? To understand that, let's take a look at how variadic functions work. Taking a step back, we'll start by taking a look at what a process looks like in memory -- why we need to do that will become clear a bit further down. We begin with the memory layout of a program in Unix often illustrated as shown below: +---------------------------+ <-- high address | command-line arguments | | and environment variables | +---------------------------+ | stack | +- - - - - - - - - - - - - -+ | | | | | | | V | | | | | | | | | | | +---------------------------+ | shared memory area | +---------------------------+ | | | | | | | | | | | | | ^ | | | | | | | +- - - - - - - - - - - - - -+ | heap | +---------------------------+ | uninitialized data or bss | | (Block Started by Symbol) | +---------------------------+ | initialized data | +---------------------------+ | text segment | +---------------------------+ <-- low address We can write a program to verify that this is a reasonable representation and at the same time illustrate each segment by printing out the memory addresses of various parts of our program, memory-layout.c: #define ARRAY_SIZE 40000 #define MALLOC_SIZE 100000 #define SHM_SIZE 100000 #define SHM_MODE 0600 char array[ARRAY_SIZE]; char *string = "a string"; char *string2; int num = 10; int num2; extern char **environ; char **argv; int argc; void func(int); void func2(const char *); int main(int argc, char **argv, char **envp) { int shmid; char *ptr; char func_array[ARRAY_SIZE]; printf("High address (args and env):

"); printf("----------------------------

"); printf("environ[0] at : 0x%12lX

", (unsigned long)environ); printf("envp[0] at : 0x%12lX

", (unsigned long)envp); printf("last arg at : 0x%12lX

", (unsigned long)&argv[argc]); printf("argc at : 0x%12lX

", (unsigned long)&argc); printf("argv at : 0x%12lX

", (unsigned long)&argv); printf("envp at : 0x%12lX

", (unsigned long)&envp); printf("

"); if (setenv("FOO", "bar", 1) < 0) { fprintf(stderr, "Unable to setenv(3): %s

", strerror(errno)); exit(1); } printf("After setenv(3):

"); printf("environ[0] at : 0x%12lX

", (unsigned long)environ); printf("envp[0] at : 0x%12lX

", (unsigned long)envp); printf("

"); printf("Stack:

"); printf("------

"); printf("Main:

"); printf("First variable (int shmid) inside main at : 0x%12lX

", (unsigned long)&shmid); printf("Second variable (char *ptr) inside main at : 0x%12lX

", (unsigned long)&ptr); printf("func_array[] ends at : 0x%12lX

", (unsigned long)&func_array[ARRAY_SIZE]); printf("func_array[] (like 'array[]', but on stack) begins at : 0x%12lX

", (unsigned long)&func_array[0]); func(0); printf("

"); printf("Shared memory:

"); printf("--------------

"); if ((shmid = shmget(IPC_PRIVATE, SHM_SIZE, SHM_MODE)) < 0) { fprintf(stderr, "Unable to get shared memory: %s

", strerror(errno)); exit(1); } if ((ptr = shmat(shmid, 0, 0)) == (void *)-1) { fprintf(stderr, "Unable to map shared memory: %s

", strerror(errno)); exit(1); } printf("shared memory attachment ends at : 0x%12lX

", (unsigned long)ptr+SHM_SIZE); printf("shared memory attachment begins at : 0x%12lX

", (unsigned long)ptr); if (shmctl(shmid, IPC_RMID, 0) < 0) { fprintf(stderr, "shmctl error: %s

", strerror(errno)); exit(1); } printf("

"); printf("Heap:

"); printf("-----

"); if ((ptr = malloc(MALLOC_SIZE)) == NULL) { fprintf(stderr, "Unable to allocate memory: %s

", strerror(errno)); exit(1); } printf("malloced area ends at : 0x%12lX

", (unsigned long)ptr+MALLOC_SIZE); printf("malloced area begins at : 0x%12lX

", (unsigned long)ptr); free(ptr); printf("

"); printf("Uninitialized Data (BSS):

"); printf("-------------------------

"); printf("array[] ends at : 0x%12lX

", (unsigned long)&array[ARRAY_SIZE]); printf("array[] (uninitialized, fixed-size char * on BSS) from : 0x%12lX

", (unsigned long)&array[0]); printf("num2 (uninitialized global int) at : 0x%12lX

", (unsigned long)&num2); printf("string2 (uninitialized global char *) at : 0x%12lX

", (unsigned long)&string2); printf("extern **environ at : 0x%12lX

", (unsigned long)&environ); printf("

"); printf("Initialized Data:

"); printf("-----------------

"); printf("num (initialized global int) at : 0x%12lX

", (unsigned long)&num); printf("string (initialized global char *) at : 0x%12lX

", (unsigned long)&string); printf("

"); printf("Text Segment:

"); printf("-------------

"); printf("func2 (function) at : 0x%12lX

", (unsigned long)&func2); printf("func (function) at : 0x%12lX

", (unsigned long)&func); printf("main (function) at : 0x%12lX

", (unsigned long)&main); printf("

"); return 0; } void func(int i) { int fint; /* Change this value to 0 and note how * the location of where it is stored * changes from the Data to BSS segment. */ static int n = 1; char *msg = "from func"; if (i) { msg = "recursive"; } printf("

Func:

"); printf("func (called %d times): frame at : 0x%12lX

", n, (unsigned long)&fint); printf("func arg (int i) at : 0x%12lX

", (unsigned long)&i); printf("static int n within func at : 0x%12lX

", (unsigned long)&n); n++; func2(msg); } void func2(const char *how) { int fint; printf("

Func2:

"); printf("func2 (%s): frame at : 0x%12lX

", how, (unsigned long)&fint); printf("func2 arg (char *how) at : 0x%12lX

", (unsigned long)&how); #ifdef STACKOVERFLOW func(1); #endif } (We threw in an opportunity to illustrate a stack overflow; compile with -DSTACKOVERFLOW to trigger it. We're also using the three-argument call to main to compare the function arg envp and the extern char **environ , and we do indeed observe a difference in behavior after calling setenv(3) : only environ gets updated, while envp remains unchanged.) Running this yields the following results, aligned with out representation of the memory layout: High address (args and env): ---------------------------- +---------------------------+ environ[0] at : 0x7F7FFF5BBFF8 | environment variables | envp[0] at : 0x7F7FFF5BBFF8 | | last arg at : 0x7F7FFF5BBFF0 | | argc at : 0x7F7FFF5B235C | command-line arguments | argv at : 0x7F7FFF5B2350 | | envp at : 0x7F7FFF5B2348 | | | | After setenv(3): | | environ[0] at : 0x70FF74F05080 | (note: now points to heap | envp[0] at : 0x7F7FFF5BBFF8 | but envp[0] remains) | | | Stack: +---------------------------+ ------ | | Main: | | First variable (int smid) inside main at : 0x7F7FFF5BBFAC | frame #2 | Second variable (char *ptr) inside main at : 0x7F7FFF5BBFA0 | | func_array[] ends at : 0x7F7FFF5BBFA0 | | func_array[] (like 'array[]', but on stack) begins at : 0x7F7FFF5B2360 | | +- - - - - - - - - - - - - -+ Func: | | func (called 1 times): frame at : 0x7F7FFF5B2324 | frame #1 | func arg (int i) at : 0x7F7FFF5B231C | | static int n within func at : 0x 601FEC |(note: static int n on bss)| +- - - - - - - - - - - - - -+ Func2: | | func2 (from func): frame at : 0x7F7FFF5B22FC | frame #0 | func2 arg (char *how) at : 0x7F7FFF5B22E8 | | +- - - - - - - - - - - - - -+ | | | | | | Shared memory: | | -------------- +---------------------------+ shared memory attachment ends at : 0x7F7FF7EFE6A0 | shared memory area | shared memory attachment begins at : 0x7F7FF7EE6000 | | +---------------------------+ | | | | Heap: | | ----- + - - - - - - - - - - - - --+ malloced area ends at : 0x70FF74F216A0 | | malloced area begins at : 0x70FF74F09000 | heap | +---------------------------+ Uninitialized Data (BSS): | | ------------------------- | | array[] ends at : 0x 60BE40 | | array[] (uninitialized, fixed-size char * on BSS) from : 0x 602200 | bss | num2 (uninitialized global int) at : 0x 6021E8 | | string2 (uninitialized global char *) at : 0x 6021E0 | | extern **environ at : 0x 6021D8 | | +---------------------------+ Initialized Data: | | ----------------- | (static int n from func) | num (initialized global int) at : 0x 601FE8 | | string (initialized global char *) at : 0x 601FE0 | | +---------------------------+ Text Segment: | | ------------- | | func2 (function) at : 0x 401196 | text | func (function) at : 0x 401104 | | main (function) at : 0x 400BE0 | | +---------------------------+ This is nice: we do see the various different segments at the addresses we expected them to be. This also illustrates how static variable initialization works: the variable static int n cannot possibly live on the stack, so ends up in the bss segment (if uninitialized or initialized to zero) or the initialized data segment. The call to setenv(3) is also interesting: given that the environment variables are supposed to be located at the very top of the process's address space, and given that the space there is necessarily limited, we can't just expand the environment without limits. Adding a new variable may thus require allocation of a new chunk of memory from the heap, copying the original array of elements there. Let's take a quick detour and review how that works, again comparing envp and environ . We use the following program, env-changes.c: extern char **environ; int main(int argc, char **argv, char **envp) { int i; char *e; printf("At startup:

"); printf("environ at : 0x%12lX

", (unsigned long)&environ); printf("*environ at : 0x%12lX

", (unsigned long)environ); printf("environ[0] at : 0x%12lX

", (unsigned long)*environ); printf("

"); printf("envp at : 0x%12lX

", (unsigned long)&envp); printf("*envp at : 0x%12lX

", (unsigned long)envp); printf("envp[0] at : 0x%12lX

", (unsigned long)*envp); printf("

environ contains:

"); i = 0; while ((e = environ[i++]) != NULL) { printf("%s at : 0x%12lX

", e, (unsigned long)e); } printf("

"); printf("envp contains:

"); i = 0; while ((e = envp[i++]) != NULL) { printf("%s at : 0x%12lX

", e, (unsigned long)e); } if (setenv("VAR3", "avocado", 1) < 0) { fprintf(stderr, "Unable to setenv(3): %s

", strerror(errno)); exit(1); } printf("

After setenv(3):

"); printf("environ at : 0x%12lX

", (unsigned long)&environ); printf("*environ at : 0x%12lX

", (unsigned long)environ); printf("environ[0] at : 0x%12lX

", (unsigned long)environ); printf("

"); printf("envp at : 0x%12lX

", (unsigned long)&envp); printf("envp at : 0x%12lX

", (unsigned long)&envp); printf("envp[0] at : 0x%12lX

", (unsigned long)envp); printf("

environ contains:

"); i = 0; while ((e = environ[i++]) != NULL) { printf("%s at : 0x%12lX

", e, (unsigned long)e); } printf("

"); printf("envp contains:

"); i = 0; while ((e = envp[i++]) != NULL) { printf("%s at : 0x%12lX

", e, (unsigned long)e); } return 0; } We run this with a cleaned up environment to reduce clutter and observe the following: $ cc -Wall -Werror env-changes.c $ env -i VAR1="lettuce" VAR2="tomato" ./a.out argc at : 0x7F7FFFEBC5EC -----+ +--------------------------------+ <-- high address argv at : 0x7F7FFFEBC5E0 ----+| | +-------------+ |- || | | VAR2=tomato | 0x7F7FFFEBCB6D | \_ ENV At startup: || | | VAR1=lettuce| 0x7F7FFFEBCB60 | / environ at : 0x 601458 -+ || | +-------------+ |- *environ at : 0x7F7FFFEBC648 | || | *environ 0x7F7FFFEBC648 | <------------------+<-+ environ[0] at : 0x7F7FFFEBCB60 | || + - - - - - - - - - - - - - - - -+ | | | || |+------------------------------+|- | | envp at : 0x7F7FFFEBC5D8 --+ |+---> || argc 0x7F7FFFEBC5EC|| \ | | *envp at : 0x7F7FFFEBC648 || +----> || argv 0x7F7FFFEBC5E0|| -- args to main | | envp[0] at : 0x7F7FFFEBCB60 |+------> || envp 0x7F7FFFEBC5D8|| / | | | || *envp||--------------------+ | environ contains: | |+------------------------------+| | VAR1=lettuce at : 0x7F7FFFEBCB60 | | stack | | VAR2=tomato at : 0x7F7FFFEBCB6D | + - - - - - - - - - - - - - - - -+ | | | | | envp contains: | | | | VAR1=lettuce at : 0x7F7FFFEBCB60 | | | | VAR2=tomato at : 0x7F7FFFEBCB6D | | | | | | | | | + - - - - - - - - - - - - - - - -+ | [output elided] | | heap | | | +--------------------------------+ | +-------> | extern char **environ 0x601458 |-----------------------+ | | | bss | +--------------------------------+ | initialized data | +--------------------------------+ | text | +--------------------------------+ We break up the output to better illustrate the change after we call setenv(3) : +--------------------------------+ <-- high address [previous output elided] | +-------------+ | +------> | | VAR2=tomato | 0x7F7FFFEBCB6D | <----------------------+ After setenv(3): +|------> | | VAR1=lettuce| 0x7F7FFFEBCB60 | <---------------------+| environ at : 0x 601458 || | +-------------+ | || *environ at : 0x741D4F305080 || | *environ (old) 0x7F7FFFEBC648 | <------------------+ || environ[0] at : 0x7F7FFFEBCB60 || + - - - - - - - - - - - - - - - -+ | || || |+------------------------------+|- | || envp at : 0x7F7FFFEBC5D8 || || argc 0x7F7FFFEBC5EC|| \ | || *envp at : 0x7F7FFFEBC648 || || argv 0x7F7FFFEBC5E0|| -- args to main | || envp[0] at : 0x7F7FFFEBCB60 || || envp 0x7F7FFFEBC5D8|| / | || || || *envp||--------------------+ || environ contains: || |+------------------------------+| || VAR1=lettuce at : 0x7F7FFFEBCB60 -+| | stack | || VAR2=tomato at : 0x7F7FFFEBCB6D --+ + - - - - - - - - - - - - - - - -+ || VAR3=avocado at : 0x741D4F3070A1 ---+ | | || | | | || envp contains: | | | || VAR1=lettuce at : 0x7F7FFFEBCB60 | | | || VAR2=tomato at : 0x7F7FFFEBCB6D | + - - - - - - - - - - - - - - - -+ || | | heap | || +-----> | environ[2] 0x741D4F3070A1 | || | environ[1]|-----------------------+| | environ[0]|------------------------+ | *environ 0x741D4F305080 |<--+ +--------------------------------+ | | extern char **environ 0x601458 |---+ | | | bss | +--------------------------------+ | initialized data | +--------------------------------+ | text | +--------------------------------+ Here we see that envp , when passed into the program, points to the same location as the extern char **environ , but that e.g., setenv(3) only operates on environ . If you were to attempt to manipulate environ yourself (by e.g., iterating over the pointer directly via while ((e = *environ++) != NULL) { ), your program would likely segfault on the subsequent setenv(3) call, as POSIX stipulates: Any application that directly modifies the pointers to which the environ variable points has undefined behavior. Having a good idea of how the default environment is set up, let's dig a bit further into how arguments are passed. (Remember, we were going to investigate the execl(3) failure in a variadic context. I know, that was quite a bit further up, but I promise we'll get back to that.) This time, however, we'll step away from using the above printf based analysis and use our good old friend gdb(1) instead, since we want to take a closer look not only at the specific variables, but, it turns out, also at the actual registers. Using gdb(1) also makes the program we use to debug this notably simpler: void add(int arg1, int arg2) { int local; local = arg1 + arg2; return; } int main(int argc, char **argv) { printf("%d

", add(1, 2)); return 0; } In older architectures, the function arguments were pushed onto the stack in reverse order, allowing you to trivially pop off the arguments as you reference them. On the AMD64 architecture, however, some of the arguments are passed in registers, so let's pull up the official ELF x86-64 System V Application Binary Interface docs (previously found at http://www.x86-64.org ). Some of the registers we're interested in are: %rax -- register a extended; containing the return value of the function

-- register a extended; containing the return value of the function %rbp -- the register base pointer, pointing to the beginning of the stack frame

-- the register base pointer, pointing to the beginning of the stack frame %rdi -- used to pass the 1st argument to functions

-- used to pass the 1st argument to functions %rsi -- used to pass the 2nd argument to functions

-- used to pass the 2nd argument to functions %rsp -- the register stack pointer, always pointing to the end of the latest allocated stack frame, i.e., the lowest address In the previous examples, we've used the address of the first local variable in a function to identify the location on the stack. This was fine for our general understanding, but a bit inaccurate. Let's look more closely: $ cc -g func-args.c $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) break main Breakpoint 1 at 0x400899: file func-args.c, line 18. +-----------------------------------------+ (gdb) break add | ENV | Breakpoint 2 at 0x40087a: file func-args.c, line 12. +-----------------------------------------+ (gdb) run +----> | 0x7f7fff2f6a34 int argc | Starting program: /home/jschauma/a.out | |+-------------------------------------+ | +----------------------------------+ ||0x7f7fff2f6fb8 "/home/jschauma/a.out"| | Breakpoint 1, main (argc=1, argv=0x7f7fff2f6a28) at ++-------------------------------------+ | func-args.c:18 +-----------------------> | 0x7f7fff2f6a28 char **argv | 18 printf("%d

", add(1, 2)); +-----------------------------------------+ (gdb) i r rax rbp rdi rsi rsp +----> | 0x7f7fff2f69f0 | rax 0xffffffffffffffff -1 | | | rbp 0x7f7fff2f69f0 0x7f7fff2f69f0 --------+ | frame#1 (main) | rdi 0x1 1 | | rsi 0x7f7fff2f6a28 140187718871592 | | rsp 0x7f7fff2f69e0 0x7f7fff2f69e0 -------------> | 0x7f7fff2f69e0 | (gdb) p argv +-----------------------------------------+ 1 = (char **) 0x7f7fff2f6a28 | 0x7f7fff2f69d8 return address | (gdb) p argv[0] +-----------------------------------------+ $2 = 0x7f7fff2f6fb8 "/home/jschauma/a.out" +-+--> | 0x7f7fff2f69d0 frame#0 (add) | (gdb) p argv[1] | | +-----------------------------------------+ $3 = 0x0 | | | ('add' is a "leaf" function, so can | (gdb) n | | | use the "red zone" as their entire | | | | stack frame) | Breakpoint 2, add (arg1=1, arg2=2) at func-args.c:12 | | | | 12 local = arg1 + arg2; | | | | (gdb) i r rax rbp rdi rsi rsp | |+-> | 0x7f7fff2f69bc 1 | rax 0xffffffffffffffff -1 | ||+> | 0x7f7fff2f69b8 2 | rbp 0x7f7fff2f69d0 0x7f7fff2f69d0 --------+ ||| | | rdi 0x1 1 ||| | 0x7f7fff2f6a50 %rbp + 128 | rsi 0x2 2 ||| +-----------------------------------------+ rsp 0x7f7fff2f69d0 0x7f7fff2f69d0 ----------+|| | | (gdb) p &arg1 || | | $4 = (int *) 0x7f7fff2f69bc -------------------------------+| (gdb) p &arg2 | $5 = (int *) 0x7f7fff2f69b8 --------------------------------+ (gdb) n 13 return local; (gdb) 14 } (gdb) i r rax rax 0x3 3 # we return 3 (gdb) n 3 # output from 'printf("%d

", add(1, 2));' main (argc=1, argv=0x7f7fff2f6a28) at func-args.c:19 19 return 0; # before we 'return'... (gdb) i r rax rax 0x2 2 # %rax contains '2', the return value of the above 'printf' call (gdb) n 20 } (gdb) i r rax rax 0x0 0 (gdb) c Continuing. [Inferior 1 (LWP 0) exited normally] Here we see arguments getting passed from the registers; since add is a "leaf" function (a function that calls no other functions), it can use the entire "red zone" (the the 128-byte area beyond %rsp ) as its stack frame, and so arg1 and arg2 are found there. The x86-64 architecture allows for storing of the first six arguments in registers ( %rdi , %rsi , %rdx , %rcx , %r8 , and %r9 ), but how does it handle functions with more than six arguments? Let's have a look, this time again switching back to printf displaying of the values, thereby turning add into a non-leaf function and forcing a more common stack layout: int add(int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7, int arg8, int arg9) { int local; /* Look, Ma, we can inspect registers from * inside our program! */ register uint64_t myrbp asm("rbp"); register uint64_t myrsp asm("rsp"); printf("Func1:

"); printf("-----

"); printf("rbp at : 0x%12lX

", (unsigned long)myrbp); printf("rsp at : 0x%12lX

", (unsigned long)myrsp); printf("

"); printf("first local var in func1 at : 0x%12lX

", (unsigned long)&local); printf("1st arg to func at : 0x%12lX

", (unsigned long)&arg1); printf("2nd arg to func at : 0x%12lX

", (unsigned long)&arg2); printf("3rd arg to func at : 0x%12lX

", (unsigned long)&arg3); printf("4th arg to func at : 0x%12lX

", (unsigned long)&arg4); printf("5th arg to func at : 0x%12lX

", (unsigned long)&arg5); printf("6th arg to func at : 0x%12lX

", (unsigned long)&arg6); printf("7th arg to func at : 0x%12lX

", (unsigned long)&arg7); printf("8th arg to func at : 0x%12lX

", (unsigned long)&arg8); printf("9th arg to func at : 0x%12lX

", (unsigned long)&arg9); local = arg1 + arg2 + arg3 + arg4 + arg5 + arg6 + arg7 + arg8 + arg9; return local; } int main(int argc, char **argv) { int local; register uint64_t rbp asm("rbp"); register uint64_t rsp asm("rsp"); printf("Main:

"); printf("-----

"); printf("rbp (in main) at : 0x%12lX

", (unsigned long)rbp); printf("rsp (in main) at : 0x%12lX

", (unsigned long)rsp); printf("

"); printf("first local var in main at : 0x%12lX

", (unsigned long)&local); printf("

"); (void)add(1, 2, 3, 4, 5, 6, 7, 8 ,9); return 0; } $ cc -Wall -Werror -g func-args2.c $ ./a.out +----------------------------------+ Main: | ENV + args | ----- +----------------------------------+ rbp (in main) at : 0x7F7FFF748010 -----> | 0x7F7FFF748010 | rsp (in main) at : 0x7F7FFF747FD0 --+ +> | 0x7F7FFF74800C | | | | | first local var in main at : 0x7F7FFF74800C --|-+ | frame#1 (main) | | | | | | | | | | | | | +---------------------------|--> | 0x7F7FFF747FE0 arg9 | | +-------------------------|--> | 0x7F7FFF747FD8 arg8 | | | +---------------------> +--> | 0x7F7FFF747FD0 arg7 | | | | +----------------------------------+ Func1: | | | | 0x7F7FFF747FC8 return address | ------ | | | +----------------------------------+ rbp at | | | : 0x7F7FFF747FC0 -----> | 0x7F7FFF747FC0 | rsp at | | | : 0x7F7FFF747F90 -+ +> | 0x7F7FFF747FBC | | | | | | | | first local var in func1 at | | | : 0x7F7FFF747FBC -|--+ | | 1st arg to func at | | | : 0x7F7FFF747FAC -|---> | 0x7F7FFF747FAC arg1 | 2nd arg to func at | | | : 0x7F7FFF747FA8 -|---> | 0x7F7FFF747FA8 arg2 | 3rd arg to func at | | | : 0x7F7FFF747FA4 -|---> | 0x7F7FFF747FA4 arg3 | 4th arg to func at | | | : 0x7F7FFF747FA0 -|---> | 0x7F7FFF747FA0 arg4 | 5th arg to func at | | | : 0x7F7FFF747F9C -|---> | 0x7F7FFF747F9C arg5 | 6th arg to func at | | | : 0x7F7FFF747F98 -|---> | 0x7F7FFF747F98 arg6 | 7th arg to func at | | +--- : 0x7F7FFF747FD0 | | | 8th arg to func at | +----- : 0x7F7FFF747FD8 | | | 9th arg to func at +------- : 0x7F7FFF747FE0 +---> | 0x7F7FFF747F90 | | | | | | | Here we see the arrangement of the function arguments that are not passed via registers: the return address at %rbp + 8 separates the frames, with each memory argument found at %rbp + 16 + 8n , where n is the zero-indexed list of arguments. That is, in our case: arguments one through 6 are passed via registers

argument seven is the zero'th memory argument, so found at %rbp + 16 = 0x7F7FFF747FC0 + 0x000000000010 = 0x7F7FFF747FD0 , at the very end of the previous frame

, at the very end of the previous frame argument eight is the 1st memory argument, so found at %rbp + 16 + 8 = ...FD8 (on the previous frame)

(on the previous frame) argument nine is the 2nd memory argument, so found at %rbp + 16 + 16 = ...FE0 (on the previous frame) This means that if you know how many args there are, you can walk up the stack by offset to pick them off. Now we're getting somewhere! (Take note that the arguments you're picking off are located on the previous frame. We'll get back to that in a bit.) Now suppose you don't know how many arguments you have. Wait, why would you not know this? Well, suppose your function was variadic. That is, your function migh accept two, three, four, or n arguments. How on earth are we supposed to figure that out? Let's create a variadic version (varargs.c) that tries to iterate over the varargs similar to our myexecl function above: void myexecl(const char *path, const char *arg0, ...) { int i; char *varg; va_list argp; va_start(argp, arg0); for (i = 1; (varg = va_arg(argp, char *)) != NULL; i++) { printf("arg %d = %s

", i, varg); } va_end(argp); return; } int main(int argc, char **argv) { /* Try to leave out the trailing NULL argument * and see what happens... */ myexecl("/tmp/prog", "prog name", "vararg1", "vararg2", "vararg3", "vararg4", "vararg5", "vararg6", "vararg7", NULL); return 0; } As with execl(3) , we stipulate that the list of arguments given must be terminated by a NULL pointer. This is necessary, as we don't know how many arguments we have. This is an inherent requirement in the handling of variable number of arguments. ( printf(3) , for example, does not require a trailing NULL pointer, but the first argument to printf(3) , the format string, is used to determine how many arguments must follow, which is why you'll get a compiler error if you don't specify enough arguments.) But how do we iterate over the arguments in particular? As per the stdarg(3) manual page this functionality is implemented as macros. The ABI specification defines the va_list type as: typedef struct { unsigned int gp_offset; unsigned int fp_offset; void *overflow_arg_area; void *reg_save_area; } va_list[1]; This refererences the area where the values of the registers are saved ( reg_save_area ), the area where the memory arguments on the stack are saved ( overflow_arg_area ), the offset in bytes from reg_save_area where the next general purpose register is saved ( gp_offset ) and the offset in bytes from reg_save_area where the next floating point register is saved ( fp_offset ). So let's look this in the debugger again: 01 $ cc -g func-args.c 02 $ gdb -q a.out +----------------------------------+ 03 Reading symbols from a.out...done. | ENV and args | 04 (gdb) break myexecl +----------------------------------+ 05 Breakpoint 1 at 0x4008c9: file func-args.c, line 12. | frame#1 (main) | 06 (gdb) run | | 07 Starting program: /home/jschauma/a.out | | 08 | | 09 Breakpoint 1, myexecl (path=0x400abf "/tmp/prog", arg0=0x400ab5 "prog name") | | 10 at func-args.c:12 | | 11 12 va_start(argp, arg0); | | 12 (gdb) info frame | | 13 Stack level 0, frame at 0x7f7fffffe150: | | 14 rip = 0x400902 in myexecl (func-args.c:14); saved rip = 0x4009da | | 15 called by frame at 0x7f7fffffe190 | | 16 source language c. | 0x7f7fffffe168 0x0 | arg 10 to 'myexecl' 17 Arglist at 0x7f7fffffe140, args: path=0x400abf "/tmp/prog", | 0x7f7fffffe160 0x400ac9 | frame #0 overflow_arg_area after subsequent va_arg() ---------+ 18 arg0=0x400ab5 "prog name" +---> | 0x7f7fffffe158 0x400ad1 | frame #0 overflow_arg_area after va_arg() -----+ | 19 Locals at 0x7f7fffffe140, Previous frame's sp is 0x7f7fffffe150 ---------+------|---> | 0x7f7fffffe150 0x400ad9 | frame #0 initial overflow_arg_area --+ | | 20 Saved registers: | | +----------------------------------+ | | | 21 rbp at 0x7f7fffffe140, rip at 0x7f7fffffe148 -------------------------|------|---> | 0x7f7fffffe148 | return address | | | 22 (gdb) p &path | | +----------------------------------+ | | | 23 $1 = (const char **) 0x7f7fffffe058 | | | 0x7f7fffffe140 | frame #0 base pointer | | | 24 (gdb) p &arg0 | | | | | | | 25 $2 = (const char **) 0x7f7fffffe050 | | | frame#0 (myexecl) | | | | 26 (gdb) p &varg | | | | | | | 27 $3 = (const char **) 0x7f7fffffe080 | | | | | | | 28 (gdb) n | | | 0x7f7fffffe0b8 "vararg4" | = reg_save_area + gp_offset (40) | | | 29 13 for (i = 1; (varg = va_arg(argp, char *)) != NULL; i++) { | +---|---> | 0x7f7fffffe0b0 "vararg3" | = reg_save_area + gp_offset (32) | | | 30 (gdb) p argp | | | | 0x7f7fffffe0a8 "vararg2" | = reg_save_area + gp_offset (24) | | | 31 $3 = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7f7fffffe150, --+ | | | 0x7f7fffffe0a0 "vararg1" | = reg_save_area + gp_offset (16) | | | 32 reg_save_area = 0x7f7fffffe090}} ---------------------------------------|---|---> | 0x7f7fffffe090 | | | | 33 | | | | | | | 34 | | | | | | | 35 (gdb) p argp | | | | | | | 36 $8 = {{gp_offset = 24, fp_offset = 48, overflow_arg_area = 0x7f7fffffe150, | | | | | | | 37 reg_save_area = 0x7f7fffffe090}} | | | | | | | 38 (gdb) s | | | | | | | 39 14 printf("arg %d = %s

", i, varg); | | | | | | | 40 (gdb) p argp | | | 0x7f7fffffe058 path | | | | 41 $11 = {{gp_offset = 32, fp_offset = 48, overflow_arg_area = 0x7f7fffffe150, | | | 0x7f7fffffe050 arg0 | frame #0 stack pointer | | | 42 reg_save_area = 0x7f7fffffe090}} | | +----------------------------------+ | | | 43 (gdb) p *(char **)0x7f7fffffe0b0 ----------------------------------------+ | | | | | | 44 $12 = 0x400a9d "vararg3" | | | | | | 45 (gdb) s | | | | | | 46 arg 2 = vararg2 | | | | | | 47 13 for (i = 1; (varg = va_arg(argp, char *)) != NULL; i++) { | | | | | | 49 (gdb) | | | | | | 50 14 printf("arg %d = %s

", i, varg); | | | | | | 51 (gdb) p argp | | | | | | 52 $13 = {{gp_offset = 40, fp_offset = 48, overflow_arg_area = 0x7f7fffffe150, | | | | | | 53 reg_save_area = 0x7f7fffffe090}} | | | | | | 54 (gdb) p *(char **)0x7f7fffffe150 | | | | | | 55 $20 = 0x400ad9 "vararg5" | | | | | | 56 (gdb) s | | | | | | 57 arg 4 = vararg4 | | | | | | 58 13 for (i = 1; (varg = va_arg(argp, char *)) != NULL; i++) { | | | | | | 59 (gdb) | | | | | | 60 14 printf("arg %d = %s

", i, varg); | | | | | | 61 (gdb) p argp | | | | | | 62 $29 = {{gp_offset = 48, fp_offset = 48, overflow_arg_area = 0x7f7fffffe158, | | | | | | 63 reg_save_area = 0x7f7fffffe090}} | | | | | | 64 (gdb) p *(char **)0x7f7fffffe158 --------------------------------------------+ +----------------------------------+ | | | 65 $30 = 0x400ad1 "vararg6" | heap | | | | 66 (gdb) p *(char **)0x7f7fffffe160 +----------------------------------+ | | | 67 $31 = 0x400ac9 "vararg7" ---------------------------------------------------+ | bss | | | | 68 (gdb) p *(char **)0x7f7fffffe168 | +----------------------------------+ | | | 69 $32 = 0x0 | | initialized data | | | | | +----------------------------------+ | | | | | text segment | | | | | | ... | | | | | | 0x400ad9 "vararg5" | <-------------------------------------------------------+ | | | | 0x400ad1 "vararg6" | <----------------------------------------------------------+ | +-----------> | 0x400ac9 "vararg7" | <-------------------------------------------------------------+ +----------------------------------+ Here, we can see quite nicely how our variable arguments are set up: the specified, non-variadic arguments path and arg0 are passed via registers as before. Any subsequent arguments passed are then stored in the reg_save_area ; to access those, we need to use the stdarg(3) macros and the va_list struct. The call to va_start initializes the struct based on the location of the last non-variadic argument ( arg0 in our case) -- this is why you cannot have an entirely variadic function: $ cc -x c -c - <<EOF void func(...); EOF <stdin>:1:11: error: ISO C requires a named argument before '...' In our example above, va_start calculaes the location of the first variadic argument (" vararg1 ") at offset 16 from the reg_save_area (as shown in gdb output line number 31) to be at 0x7f7fffffe090 plus decimal 16 / hex 10 = 0x7f7fffffe0a0 . After the first execution of the for loop conditions, varg is assigned the value from that location, and the gp_offset incremented by the suitable amount for the given data type (i.e., sizeof(char *) = 8 in our case). The second variadic argument is thus found at reg_save_area (0x7f7fffffe090) + gp_offset (24) = 0x7f7fffffe0a8 , and so on. Recall that we pass the first six arguments via registers; two arguments were non-variadic, so we have four variadic arguments that we pick off via this logic ( reg_save_area + gp_offset , adjusted upon each subsequent call to va_arg ). The next variadic arguments are then found in the overflow_arg_area . This overflow area sits on the previous function's frame, and as before, all additional arguments are pushed onto the stack, beginning right at the previous frame's stack pointer ( 0x7f7fffffe150 in our case, which contains the address of the string "vararg5" found in the text segment). As we call va_arg , the overflow_arg_area is adjusted, as illustrated in lines 62ff. Lines 68/69 explicitly show our 10th argument to myexecl being NUL , which in turn is explicitly used by our code to know when to stop picking off arguments from the overflow area. What happens if we leave out the trailing NUL ? Well... $ gdb -q ./a.out Reading symbols from ./a.out...done. (gdb) run Starting program: /home/jschauma/a.out arg 1 = vararg1 arg 2 = vararg2 arg 3 = vararg3 arg 4 = vararg4 arg 5 = vararg5 arg 6 = vararg6 arg 7 = vararg7 arg 8 = �� arg 9 = �� Program received signal SIGSEGV, Segmentation fault. 0x000070da3b2f10a1 in strlen () from /usr/lib/libc.so.12 (gdb) bt #0 0x000070da3b2f10a1 in strlen () from /usr/lib/libc.so.12 #1 0x000070da3b2e4d4e in __vfprintf_unlocked_l () from /usr/lib/libc.so.12 #2 0x000070da3b2e64ca in vfprintf () from /usr/lib/libc.so.12 #3 0x000070da3b2e0528 in printf () from /usr/lib/libc.so.12 #4 0x0000000000400920 in myexecl (path=0x400aaf "/tmp/prog", arg0=0x400aa5 "prog name") at func-args3.c:14 #5 0x00000000004009d1 in main (argc=1, argv=0x7f7fff0d19a8) at func-args3.c:25 (gdb) frame 4 #4 0x0000000000400920 in myexecl (path=0x400aaf "/tmp/prog", arg0=0x400aa5 "prog name") at func-args3.c:14 14 printf("arg %d = %s

", i, varg); (gdb) p argp $1 = {{gp_offset = 48, fp_offset = 48, overflow_arg_area = 0x7f7fff0d1970, reg_save_area = 0x7f7fff0d1880}} (gdb) p *(char **)0x7f7fff0d1970 $2 = 0x600e00 <environ> "\270\031\r\377\177\177" (gdb) p *(char **)0x7f7fff0d1968 $3 = 0x1004005f9 <error: Cannot access memory at address 0x1004005f9> (gdb) p *(char **)0x7f7fff0d1960 $4 = 0x7f7fff0d19a8 "8\037\r\377\177\177" (gdb) p *(char **)0x7f7fff0d1958 $5 = 0x7f7fff0d2fe0 "\250\031\r\377\177\177" (gdb) p *(char **)0x7f7fff0d1950 $6 = 0x400ab9 "vararg7" (gdb) After iterating over the arguments as before, our for loop does not encounter NUL , so keeps incrementing the overflow_arg_area after retrieving " vararg7 ". This works out (for some value of "works out" in that it doesn't immediately segfault) twice before eventually the memory address pointed to at the updated overflow_arg_area lies within an area that you have no business accessing - SIGSEGV , core dump, game over. As we see above, we need a way to let va_arg know where to start (by providing at least a single non-variadic argument) and where to finish (by specifying a boundary condition). What happens if we specify no variadic arguments, i.e., if we call myexecl("/tmp/prog", (char *)0); ? For starters, note that the compiler does not complain, even if you pass -Wall -Werror -Wextra (as you should). This is different from calling execl(3) with only two arguments: $ cc -Wall -g func-args4.c func-args4.c: In function 'main': func-args4.c:25:2: warning: not enough variable arguments to fit a sentinel [-Wformat=] execl("/tmp/prog", (char *)0); ^ In other words, execl(3) is special. The compiler happens to know that execl(3) , even though it accepts a variable number of arguments, requires at least one variadic argument. This is compiler specific, as can be verified by trying to compile the same code with a different compiler than our default GCC (such as e.g., pcc or clang). Lesson 3: In C, a variadic function requires at least one non-variadic argument. Your compiler does not necessarily know this. But let's see what happens when we do in fact not pass any arguments, returning to our code from above, stepping through the execution once more: $ cc -g fail.c +----------------------------------+ $ gdb -q ./a.out | | Reading symbols from ./a.out...done. | ENV and args | (gdb) break 37 | | Breakpoint 1 at 0x400aab: file fail.c, line 37. +----------------------------------+ (gdb) run | 0x7f7fffa04350 | Starting program: /home/jschauma/a.out | | | frame#1 (main) | Breakpoint 1, myexecl (name=0x400d06 "/tmp/prog", arg=0x0) at fail.c:37 | | 37 for (i = 2; va_arg(ap, char *) != NULL; i++) { | | (gdb) p ap | | $1 = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7f7fffa04330, | | reg_save_area = 0x7f7fffa04270}} | | (gdb) p *(char **)0x7f7fffa04280 | | $2 = 0x7f7fffa04388 "\035I\240\377\177\177" | | (gdb) p *(char **)0x7f7fffa04288 | | $3 = 0x601210 <__progname> "\027I\240\377\177\177" | | (gdb) p *(char **)0x7f7fffa04290 | | $4 = 0x0 | | [...] | | 49 for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++) { | 0x7f7fffa04330 | frame #0 initial overflow_arg_area (gdb) +----------------------------------+ 54 r = execve(name, argv, environ); | 0x7f7fffa04328 | return address (gdb) p argv +----------------------------------+ $5 = (char **) 0x7f7fffa04200 | 0x7f7fffa04320 | frame#0 base pointer (gdb) p *argv | frame#0 (myexecl) | $6 = 0x0 | | (gdb) p 0x7f7fffa04200 | 0x7f7fffa04290 0x0 | NULL -------------------------------+ $7 = 140187726266880 | 0x7f7fffa04288 0x601210 | vararg 2 (if we had passed it) ----+ | (gdb) p *0x7f7fffa04200 | 0x7f7fffa04280 0x7f7fffa04388 | vararg 1 (if we had passwd it) -+ | | $8 = 0 | 0x7f7fffa04270 | reg_save_area | | | (gdb) p *(char **)0x7f7fffa04200 | | | | | $9 = 0x0 | 0x7f7fffa04238 0x400d06 | 'name' (1st argument to myexecl)| | | (gdb) p *(char **)0x7f7fffa04208 | 0x7f7fffa04230 0x0 | 'arg' (2nd argument to myexecl) | | | $10 = 0x7f7fffa04388 "\035I\240\377\177\177" | | | | | (gdb) p *(char **)0x7f7fffa04210 | | | | | $11 = 0x601210 <__progname> "\027I\240\377\177\177" | | | | | (gdb) p *(char **)0x7f7fffa04218 | 0x7f7fffa04218 0x0 | argv[3] ------------------------|--|--+ $12 = 0x0 | 0x7f7fffa04210 0x601210 | argv[2] ------------------------|--+ (gdb) info frame | 0x7f7fffa04208 0x7f7fffa04388 | argv[1] ------------------------+ Stack level 0, frame at 0x7f7fffa04330: | 0x7f7fffa04200 0x0 | argv[0] (from 'arg' to 'myexecl') rip = 0x400c05 in myexecl (fail.c:54); saved rip = 0x400a14 | | called by frame at 0x7f7fffa04350 | | source language c. | | Arglist at 0x7f7fffa04320, args: name=0x400d06 "/tmp/prog", arg=0x0 | | Locals at 0x7f7fffa04320, Previous frame's sp is 0x7f7fffa04330 Saved registers: rbp at 0x7f7fffa04320, rip at 0x7f7fffa04328 With no variadic arguments passed, we loop and let va_arg fetch values from the register save area until it happens to encounter NUL , meaning we are then constructing an argv that will contain: argv[0] 0x0 from arg0 passed to myexecl argv[1] \035I\240\377\177\177 whatever was found at 0x7f7fffa04388 (the address found at 0x7f7fffa04280 , reg_save_area = 0x7f7fffa04270 + gp_offset = 16 ) argv[2] \027I\240\377\177\177 whatever was found at 0x601210 (the address found at 0x7f7fffa04288 , reg_save_area = 0x7f7fffa04270 + gp_offset = 24 ) argv[3] 0x0 whatever was found at 0x7f7fffa04290 , reg_save_area = 0x7f7fffa04270 + gp_offset = 32 ) We then call execve(2) with that argv . Now you might think that having argv[0] set to NUL might be a problem, but it actually is not, at least not directly for our program: argv[0] is not used during the execution, really. Rather, it is merely used to set the internal program name, which, on BSD systems at least (and I think on Linux systems as well), happens to be to available as __progname . The C runtime library crt0 's __start routine sets it to the basename of argv[0] ; if we pass NUL , all that happens is that __progname becomes a NULL pointer: $ cat prog.c #include <stdio.h> #include <unistd.h> extern char *__progname; int main(int argc, char **argv) { int i; printf("__progname: |%s|



", __progname); printf("Args (if any):

"); for (i=1; i<argc; i++) { printf("%d: %s

", i, argv[i]); } printf("

"); return 0; } $ cc -Wall -Werror -Wextra prog.c -o /tmp/prog $ /tmp/prog __progname: |prog| Args (if any): $ cc -Wall -Werror -Wextra fail.c -o fail $ ./fail __progname: || Args (if any): $ As we see, passing NUL as argv[0] is not an issue. So let's get back to our original problem and (continue to) try to figure out why our execution failed depending on whether we called e.g., setenv(3) or printf(3) prior to (erroneously) calling execl(3) with only two arguments. For that, we return to a trivial shell script as /tmp/prog : $ cat /tmp/prog #! /bin/sh i=1 echo "${0} invoked with args:" for arg in "$@"; do echo "${i}: ${arg}" i=$(( i + 1 )) done echo env $ cc -g -Wall -Werror -Wextra fail.c -o fail $ env -i VAR1="lettuce" VAR2="tomato" ./fail /tmp/prog invoked with args: 1: 2: ��� PWD=/home/jschauma VAR1=lettuce VAR2=tomato $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 Unable to execl: Bad address $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 2 We set FOO to 'bar'. /tmp/prog invoked with args: FOO=bar PWD=/home/jschauma VAR1=lettuce VAR2=tomato $ Yep, that's right: we're back where we started! But now we know how the argv was constructed, at least in the very first case (no arguments passed to fail , so no call to setenv(3) or printf(3) ), and so we are not surprised to find the output of the first ./fail invocation to show us two garbage arguments (constructed from the bogus reg_save_area ). But then why do we get the Bad address error when we call setenv(3) ? Let's debug that call: $ gdb -q ./fail | Let's look at what happens here. Reading symbols from ./fail...done. | Recall that the first arguments are (gdb) break main | passed via the registers %rdi, %rsi Breakpoint 1 at 0x4009af: file fail.c, line 14. | %rdx, %rcx, %r8, and %r9. (gdb) break setenv | Breakpoint 2 at 0x400720 | (gdb) break myexecl | Breakpoint 3 at 0x400a7e: file fail.c, line 36. | (gdb) run 1 | Starting program: /home/jschauma/fail 1 | | Breakpoint 1, main (argc=2, argv=0x7f7ffff0d698) at fail.c:14 | 14 if (argc > 1) { | (gdb) i r rdi rsi rdx rcx r8 r9 | rdi 0x2 2 | "first" argument to 'main': argc = 2 rsi 0x7f7ffff0d698 | "second" argument to 'main': argv rdx 0x7f7ffff0d6b0 -------+ | "third" argument to 'main': envp (even if not used!) rcx 0x601210 6296080 | | __progname is found here r8 0x0 0 | | NUL r9 0x0 0 | | NUL (gdb) p *(char **)0x7f7ffff0d6b0 -------+ | Note: we got envp as a third argument to main $3 = 0x7f7ffff0dc46 "ENV=/home/jschauma/.shrc" | even if we didn't use that prototype for 'main' (gdb) n | 15 if (setenv("FOO", "bar", 1) < 0) { | (gdb) | | Breakpoint 2, 0x000075f816e82799 in setenv () from /usr/lib/libc.so.12 | Ok, now we're in setenv(3), which was called (gdb) i r rdi rsi rdx rcx r8 r9 | as 'setenv("FOO", "bar", 1)': rdi 0x400cdc 4197596 ----+ | arg1 = "FOO" rsi 0x400cd8 4197592 -+ | | arg2 = "bar" rdx 0x1 1 | | | arg3 = 1 rcx 0x601210 6296080 | | | unchanged from before r8 0x0 0 | | | r9 0x0 0 | | | (gdb) x/s 0x400cdc ----------|--+ | 0x400cdc: "FOO" | | (gdb) x/s 0x400cd8 ----------+ | 0x400cd8: "bar" | (gdb) n | Single stepping until exit from function setenv, which has no line number | information. | main (argc=2, argv=0x7f7ffff0d698) at fail.c:19 | 19 if (argc > 2) { | We're back in main; let's see what's left (gdb) i r rdi rsi rdx rcx r8 r9 | in the first three registers: rdi 0x75f817147460 129708399555680 | Some memory location. rsi 0x400cdc 4197596 | "FOO" rdx 0x4 4 | 4 rcx 0x0 0 | NUL r8 0xa8 168 | r9 0x5 5 | That is, whatever happened in 'setenv(3)' left (gdb) n | these values in the registers. 23 myexecl("/tmp/prog", (char *) 0); | (gdb) s | | Breakpoint 3, myexecl (name=0x400d06 "/tmp/prog", arg=0x0) at fail.c:36 | Now we're in 'myexecl,' which we called with two arguments: 36 va_start(ap, arg); | (gdb) i r rdi rsi rdx rcx r8 r9 | rdi 0x400d06 4197638 | name = "/tmp/prog" rsi 0x0 0 | arg = NUL rdx 0x4 4 | We didn't pass any other args, so the following are left unchanged. rcx 0x0 0 | r8 0xa8 168 | r9 0x5 5 | (gdb) n | 37 for (i = 2; va_arg(ap, char *) != NULL; i++) { | (gdb) p ap | Now look at where our 'reg_save_area' starts. $4 = {{gp_offset = 16, fp_offset = 48, overflow_arg_area = 0x7f7ffff0d650, | reg_save_area = 0x7f7ffff0d590}} | (gdb) x 0x7f7ffff0d5a0 | reg_save_area + offset => location of register %rdx 0x7f7ffff0d5a0: "\004" | (gdb) n | 42 if ((argv = alloca(i * sizeof (char *))) == NULL) { | (gdb) | 47 va_start(ap, arg); | (gdb) | 48 argv[0] = __UNCONST(arg); | (gdb) | 49 for (i = 1; (argv[i] = va_arg(ap, char *)) != NULL; i++) { | So now we copy whatever we find in 'reg_save_area' until (gdb) | we encounter NULL, which was left in %rcx. 54 r = execve(name, argv, environ); | (gdb) p argv | That is, we now have constructed our argv as: $8 = (char **) 0x7f7ffff0d520 | (gdb) p *(char **)0x7f7ffff0d520 | argv[0] = NUL (from 'arg') $9 = 0x0 | (gdb) p *(char **)0x7f7ffff0d528 | argv[1] = 0x4 (picked via va_arg from left over %rdx) $10 = 0x4 <error: Cannot access memory at address 0x4> | (gdb) p *(char **)0x7f7ffff0d530 | $11 = 0x0 | argv[2] = 0x0 (picked via va_arg from left over %rcx) (gdb) c | Continuing. | Unable to execl: Bad address | argv[1] = 0x4 is not a valid address => EFAULT | So the reason that we fail whenever we call setenv(3) prior to execl(3) is because of the value 0x4 left in the register. But why is that value 0x4 , and what left it there? Once again, to the source! setenv(3) , to be precise: int setenv(const char *name, const char *value, int rewrite) { size_t l_name, l_value, length; ssize_t offset; char *envvar; ... l_value = strlen(value); ... /* Allocate memory for name + `=' + value + NUL. */ if ((envvar = __allocenvvar(length)) == NULL) goto bad; if (environ[offset] != NULL) __freeenvvar(environ[offset]); environ[offset] = envvar; (void)memcpy(envvar, name, l_name); envvar += l_name; *envvar++ = '='; copy: (void)memcpy(envvar, value, l_value + 1); good: (void)__unlockenv(); return 0; bad: (void)__unlockenv(); return -1; } The last function call within setenv(3) that passes parameters is ' (void)memcpy(envvar, value, l_value + 1); ', so we should find whatever memcpy(3) (and __unlockenv ) left in the registers. As we track the arguments, we find that l_value + 1 is left in register %rdx unchanged: l_value is just the length of value , so not surprisingly, we get strlen("bar") + 1 = 4 in %rdx . We now know why setenv(3) followed by execl("/tmp/prog", (char *)0) fails: setenv(3) left a numeric value in %rdx , the register used for the third argument, and the variadic execl(3) picked up that argument and constructed an argv to pass to execve(2) , which then attempts to access 0x4 as the second argument (after its argv[0] = 0x0 ), leading to EFAULT , as expressed in errno(2) : 14 EFAULT Bad address. The system detected an invalid address in attempting to use an argument of a call. Now explaining why things change and suddenly work out again when we add a printf(3) in between setenv(3) and execl(3) is not that hard: printf(3) , itself being variadic, leads to the values left in the third argument register to be NUL , so that va_arg inside execl(3) behaves as if it had been invoked as execl("/tmp/prog", (char *)0, (char *)0) . You can verify this by changing the printf(3) call to any other function: if the function uses a third argument whatever is left in that register will influence how execl(3) will behave: if NUL is left in %rdx , the argv to execve(2) will be argv[0] = 0x0, argv[1] = 0x0 and succeed; if %rdx contains a value V , the argv constructed will be argv[0] = 0x0, argv[1] = V and execve(2) will fail. Hooray, we got to the bottom of... wait, not so fast! We're not done: as we're debugging this issue, we've used a shell script as the program to execl(3) . Suppose we had used a binary: $ cp `which env` /tmp/prog $ cc -g fail.c -o fail $ env -i VAR1="lettuce" VAR2="tomato" ./fail VAR1=lettuce VAR2=tomato $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 VAR1=lettuce VAR2=tomato FOO=bar $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 2 We set FOO to 'bar'. VAR1=lettuce VAR2=tomato FOO=bar $ Wait... what? Now what's going on? Didn't we establish that ./fail 1 should, uhm, fail? Let's double check: $ cat >/tmp/prog.sh <<EOF #! /bin/sh env EOF $ chmod a+rx /tmp/prog.sh $ ln -fs /tmp/prog.sh /tmp/prog $ env -i VAR1="lettuce" VAR2="tomato" ./fail PWD=/tmp VAR1=lettuce VAR2=tomato $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 Unable to execl: Bad address $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 2 We set FOO to 'bar'. FOO=bar PWD=/tmp VAR1=lettuce VAR2=tomato $ cp `which env` /tmp/prog.bin $ ln -sf /tmp/prog.bin /tmp/prog $ env -i VAR1="lettuce" VAR2="tomato" ./fail 1 VAR1=lettuce VAR2=tomato FOO=bar $ So there's a difference between execve(2) 'ing a shell script (or any interpreter file, really) and a binary. Reference execve(2): An interpreter file begins with a line of the form: #! interpreter [arg] When an interpreter file is execve d, the system actually execve's the specified interpreter. If the optional arg is specified, it becomes the first argument to the interpreter, and the name of the originally execve'd file becomes the second argument; otherwise, the name of the originally execve'd file becomes the first argument. The original argu- ments are shifted over to become the subsequent arguments. The zeroth argument, normally the name of the execve()d file, is left unchanged. The interpreter named by interpreter must not itself be an interpreter file. (See script(7) for a detailed discussion of interpreter file exe- cution.) The script(7) manual page does indeed have all the details and is worth perusing to really understand the execution of interpreter files, but for our discussion the relevant difference is that when executing an interpreter file, execve(2) munges the argv , and thus will fail when it encounters an invalid address, while during execution of a binary it hands the argv off without manipulating it, leaving it to the executable to do whatever it pleases with the given arguments. Ok, now we're really done. Phew. The initial problem was raised by a student; the journey to the answer shown in this blog post is one of the many reasons why I enjoy teaching. But let's recap: execl(3) really needs at least a third argument

really needs at least a third argument your compiler would have told you had you compiled your code with -Wall -Werror -Wextra

printf(3) debugging is useful, but may alter the behavior of the program you're debugging

debugging is useful, but may alter the behavior of the program you're debugging the debugger is your friend

visualizing the memory layout is helpful in debugging your stack frames

stdarg(3) varargs require at least one non-variadic parameter as well as a sentinel to properly identify where to pick off the values

varargs require at least one non-variadic parameter as well as a sentinel to properly identify where to pick off the values arguments from previous function calls may be left in registers

when in doubt, use the source

the rabbit hole always goes one level deeper than you first think Thanks for playing "So you think you know C" with me - see you next time! January 02, 2020 Source files used above: env-changes.c

fail.c

func-args.c

memory-layout.c

varargs.c Related links and additional reading: Useful GCC warning options not enabled by -Wall -Wextra

Wikipedia page on the Data Segment

Wikipedia page on the call stack

POSIX environ

System V Application Binary Interface

stdarg(3) manual page

How do varargs work in C?

va_arg, va_copy, va_end, va_start (Microsoft C Runtime Library docs)

X86 64 Register and Instruction Quick Start

amd64 and va_arg

NetBSD Internals: Process startup

The #! magic, details about the shebang/hash-bang mechanism on various Unix flavours

Why do some scripts start with #! ... ? (Unix FAQ)

How programs get run