This transcript will help you understand the calling conventions and the ABI. It's taken on a 64 bit system; do the same on a 32 bit system and observe the differences: root@ubuntu64:/home/sergey# cat hello.c #include int main() { printf("Hello!\n"); return 42; } Ignore the .cfi_* parts, understand the rest. Use the Tiny Guide to x86 assembly, or simply google specific instructions (it works!). When in doubt, look at the Intel instruction manuals: http://www.intel.com/content/www/us/en/processors/architectures-software-developer-manuals.html root@ubuntu64:/home/sergey# cat hello.s .file "hello.c" .section .rodata .LC0: .string "Hello!" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 movl $.LC0, %edi call puts movl $42, %eax popq %rbp .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1" .section .note.GNU-stack,"",@progbits root@ubuntu64:/home/sergey# cat exec.c #include int main() { char *const args[] = {"/bin/ls", NULL}; // see execv(3) for arguments execv("/bin/ls", args); } In the following code, understand the representation of the args[]: where and how the pointers are stored? root@ubuntu64:/home/sergey# gcc -S exec.c root@ubuntu64:/home/sergey# cat exec.s .file "exec.c" .section .rodata .LC0: .string "/bin/ls" .text .globl main .type main, @function main: .LFB0: .cfi_startproc pushq %rbp .cfi_def_cfa_offset 16 .cfi_offset 6, -16 movq %rsp, %rbp .cfi_def_cfa_register 6 subq $16, %rsp movq $.LC0, -16(%rbp) movq $0, -8(%rbp) leaq -16(%rbp), %rax movq %rax, %rsi movl $.LC0, %edi call execv leave .cfi_def_cfa 7, 8 ret .cfi_endproc .LFE0: .size main, .-main .ident "GCC: (Ubuntu/Linaro 4.6.1-9ubuntu3) 4.6.1" .section .note.GNU-stack,"",@progbits Now I am going to walk the code to the actual point where the exec() system call is made. Notice that I am cheating here, by compiling the executable statically (and with debug info). Most executables get compiled dynamically these days; static libraries no longer ship by default on most distributions. This cheat allows me to bypass an extra layer of indirection introduced by dynamic linking, though. root@ubuntu64:/home/sergey# gcc -static -g -o exec exec.c root@ubuntu64:/home/sergey# gdb ./exec GNU gdb (Ubuntu/Linaro 7.3-0ubuntu2) 7.3-2011.08 Copyright (C) 2011 Free Software Foundation, Inc. License GPLv3+: GNU GPL version 3 or later This is free software: you are free to change and redistribute it. There is NO WARRANTY, to the extent permitted by law. Type "show copying" and "show warranty" for details. This GDB was configured as "x86_64-linux-gnu". For bug reporting instructions, please see: ... Reading symbols from /home/sergey/exec...done. (gdb) b main Breakpoint 1 at 0x40104c: file exec.c, line 5. (gdb) r Starting program: /home/sergey/exec Breakpoint 1, main () at exec.c:5 5 char *const args[] = {"/bin/ls", NULL}; (gdb) disas main Dump of assembler code for function main: 0x0000000000401044 <+0>: push %rbp 0x0000000000401045 <+1>: mov %rsp,%rbp 0x0000000000401048 <+4>: sub $0x10,%rsp => 0x000000000040104c <+8>: movq $0x488be4,-0x10(%rbp) 0x0000000000401054 <+16>: movq $0x0,-0x8(%rbp) 0x000000000040105c <+24>: lea -0x10(%rbp),%rax 0x0000000000401060 <+28>: mov %rax,%rsi 0x0000000000401063 <+31>: mov $0x488be4,%edi 0x0000000000401068 <+36>: callq 0x40cfc0 0x000000000040106d <+41>: leaveq 0x000000000040106e <+42>: retq End of assembler dump. (gdb) disas execv Dump of assembler code for function execv: 0x000000000040cfc0 <+0>: mov 0x2a8941(%rip),%rdx # 0x6b5908 0x000000000040cfc7 <+7>: jmpq 0x4445b0 End of assembler dump. (gdb) disas execve Dump of assembler code for function execve: 0x00000000004445b0 <+0>: mov $0x3b,%eax 0x00000000004445b5 <+5>: syscall 0x00000000004445b7 <+7>: cmp $0xfffffffffffff000,%rax 0x00000000004445bd <+13>: ja 0x4445c1 0x00000000004445bf <+15>: repz retq 0x00000000004445c1 <+17>: mov $0xffffffffffffffb0,%rdx 0x00000000004445c8 <+24>: neg %eax 0x00000000004445ca <+26>: mov %eax,%fs:(%rdx) 0x00000000004445cd <+29>: or $0xffffffffffffffff,%rax 0x00000000004445d1 <+33>: retq End of assembler dump. Note the 'syscall' instruction. This is a new optimized x86-64 instruction that causes a software interrupt and jumps to the system call dispatcher entry in the kernel, at the same time setting the code privilege level to Ring0. The kernel entry address is stored in a Model-specific Register (MSR). Recall that in ia32 the system call used a software interrupt 0x80 ("int 0x80"), and the entry address was taken from the 0x80-th entry of the Interrupt Descriptor Table (IDT), pointed to by the dedicated IDTR register. So the new 64-bit scheme has less apparent indirection. More about 64-bit systems calls: http://blog.rchapman.org/post/36801038863/linux-system-call-table-for-x86-64 Note the hex 3b above. This is the number of the exec system call in the 64-bit table. The tables and numbers for 32-bit system calls and 64-bit system calls are completely independent, and all the numbers are different. The sys_call_table used to be a straight-up array of function pointers. In newer Linux kernels, it's generated by a shell script from a table: http://lxr.linux.no/#linux+v3.12.6/arch/x86/syscalls/syscalltbl.sh http://lxr.linux.no/#linux+v3.12.6/arch/x86/syscalls/syscall_64.tbl http://lxr.linux.no/#linux+v3.12.6/arch/x86/syscalls/syscall_32.tbl If lxr.linux.no seems sluggish, use http://lxr.free-electrons.com/, e.g., http://lxr.free-electrons.com/source/arch/x86/syscalls/syscall_64.tbl The generated syscall header file is pulled in by #include near the bottom of http://lxr.free-electrons.com/source/arch/x86/kernel/syscall_64.c Exercises: Find the system call in Linux source code (e.g., at http://lxr.linux.no/) and convince yourself that it's indeed exec(). Find the kernel function that implements exec(). Repeat compilation and interpret the results with other GCC options: -O (optimization level), -fomit-frame-pointer , -fPIC etc. See 'man gcc' if in doubt -- there are many kinds of options.