(primary url for this document is http://lightning.voshod.com/asm/startup.html)
All information provided here derived from my own research.
So, mistakes and some sort of incompleteness could exist.
If you find any - please contact me.
Please note that in general case you can apply this information only to plain assembly programs (gas/nasm); some things described here (stack/registers state) are not true for anything compiled/linked with gcc (C as well as assembly) -- gcc inserts its own startup code that is executed before control is passed to main() function.
Main source and authority of information provided below is Linux Kernel
fs/binfmt_elf.c file.
If you want all details of the startup process - go read it.
All assembly code examples use nasm syntax.
You can download program suite that was used when writing this document
here.
| Function | Kernel file | Comments |
| shell | ... | on user side one types in program name and strikes enter |
| execve() | ... | shell calls libc function |
| sys_execve() | ... | libc calls kernel... |
| sys_execve() | arch/i386/kernel/process.c | arrive to kernel side |
| do_execve() | fs/exec.c | open file and do some preparation |
| search_binary_handler() | fs/exec.c | find out type of executable |
| load_elf_binary() | fs/binfmt_elf.c | load ELF (and needed libraries) and create user segment |
| start_thread() | include/asm-i386/processor.h | and finally pass control to program code |
Figure 1. Startup process of ELF binary.
Layout of segment created for ELF binary shortly can be represented
by Figure 2. Yellow parts represent
correspondent program sections. Shared libraries are not shown here; their
layout duplicates layout of program, except that they reside in earlier
addresses.
0x08048000
| code | .text section |
| data | .data section |
| bss | .bss section |
| ...
... ... |
free space |
| stack | stack (described later) |
| arguments | program arguments |
| environment | program environment |
| program name | filename of program (duplicated in arguments section) |
| null (dword) | final dword of zero |
Figure 2. Segment layout of ELF binary.
Program takes at least two pages of memory (1 page = 4 KB), even if it consists of single sys_exit(); at least one page for ELF data (yellow color), and one for stack, arguments, and environment. Stack is growing to meet bss; also you can use memory beyond bss (.bss section) for dynamic data allocation (f.e. libc malloc() function does so).
Note: this information was gathered from fs/binfmt_elf.c,
include/linux/sched.h (task_struct.addr_limit), and core dumps investigated
with ultimate binary viewer biew).
| argc | [dword] argument counter (integer) |
| argv[0] | [dword] program name (pointer) |
| argv[1]
... argv[argc-1] |
[dword] program args (pointers) |
| NULL | [dword] end of args (integer) |
| env[0]
env[1] ... env[n] |
[dword] environment variables (pointers) |
| NULL | [dword] end of environment (integer) |
Figure 3. Stack layout of ELF binary.
Here is piece of source from kernel that proves this:
fs/binfmt_elf.c create_elf_tables()
...
put_user((unsigned long) argc, --sp);
current->mm->arg_start = (unsigned long) p;
while (argc-- > 0) {
put_user(p, argv++);
while (get_user(p++)) /* nothing */
;
}
put_user(0, argv);
current->mm->arg_end = current->mm->env_start = (unsigned long) p;
while (envc-- > 0) {
put_user(p, envp++);
while (get_user(p++)) /* nothing */
;
}
put_user(0, envp);
...
So, if you want to get arguments and environment -- you just need to pop
then one by one; argc and argv[0] are always present. Here's sample code
(it is quite useless, just shows how to do it):
pop eax ;get argument counter pop ebx ;get our name (argv[0]) .arg: pop ecx ;pop all arguments test ecx,ecx jnz .arg .env: ;pop all environment vars pop edx test edx,edx jnz .envIn fact you can also access arguments and environment in a different way -- directly. This method is based on structure of the user segment of loaded ELF binary: arguments and environment lay consequently at the end of segment (Figure 2). So, you can fetch address of first argument from the stack, and then just use it as start address. Arguments and environment variables are null-terminated strings; you need to know who is who, so you have to evaluate start and end of arguments and environment:
pop eax ;get argument counter pop esi ;start of arguments mov edi,[esp+eax*4] ;end of arguments mov ebp,[esp+(eax+1)*4] ;start of environmentSecond way seems to be more complex, you have to distinguish arguments manually. However it can be more suitable in some cases. Program name also can be fetched by downstepping from 0xBFFFFFFB (0xBFFFFFF-4) address (Figure 2).
include/asm-i386/elf.h
... /* SVR4/i386 ABI (pages 3-31, 3-32) says that when the program starts %edx contains a pointer to a function which might be registered using `atexit'. This provides a mean for the dynamic linker to call DT_FINI functions for shared libraries that have been loaded before the code runs. A value of 0 tells we have no such handler. */ #define ELF_PLAT_INIT(_r) _r->edx = 0 ...Practically, simple investigation shows that other registers have well defined values. Here we go...
If you will be patient enough and follow the path shown on Figure 1, you'll find out that pt_regs structure (that contains register values before system call) is downpassed to load_elf_binary() and create_elf_tables() in fs/binfmt_elf.c COMPLETELY UNCHANGED (I will not cover this chain and appropriate kernel sources here to save space, but do not take my words, go check it :). The only modification is done right before passing control to program code and was shown above -- EDX is zeroed (note: final start_thread() sets only segment & stack registers. Also EAX is always zero too, though I haven't found corresponding kernel source). This means that values of most general registers (EBX,ECX,ESI,EDI,EBP) on program startup are the same as in caller program before sys_execve()! More to say: one can pass to program any custom values he wants in ESI, EDI and EBP (certainly by means of direct syscall, not libc execve() function), and called program will receive them (sys_execve() call needs only EBX (program name), ECX (arguments) and EDX (environment) to be set). Conclusion: program gets photo of registers state before sys_execve(). You can use this to hack libc :)
I wrote two simple programs to illustrate state of registers - execve and regs (binaries, source). regs shows registers state on startup, execve executes given program and shows registers before sys_execve() call. You can easily combine them - try running
$ ./execve ./regs
on Linux 2.0 and you will get the picture of what I'm talking about.
include/asm-i386/elf.h
#define ELF_PLAT_INIT(_r) do { \
_r->ebx = 0; _r->ecx = 0; _r->edx = 0; \
_r->esi = 0; _r->edi = 0; _r->ebp = 0; \
_r->eax = 0; \
} while (0)
Finally, as visual illustration of this difference, here is partial output
of regs program both for Linux 2.0 and Linux 2.2:
Linux 2.0 (kernel 2.0.37)
EAX : 0x0 EBX : 0x80A1928 ECX : 0x80A1958 EDX : 0x0 ESI : 0x0 EDI : 0x8049E90 EBP : 0xBFFFFBC4 ESP : 0xBFFFFE14 EFLAGS : 0x282 CS : 0x23 DS : 0x2B ES : 0x2B FS : 0x2B GS : 0x2B SS : 0x2BLinux 2.2 (kernel 2.2.10)
EAX : 0x0 EBX : 0x0 ECX : 0x0 EDX : 0x0 ESI : 0x0 EDI : 0x0 EBP : 0x0 ESP : 0xBFFFFB40 EFLAGS : 0x292 CS : 0x23 DS : 0x2B ES : 0x2B FS : 0x0 GS : 0x0 SS : 0x2BIn fact you can use this difference to determine quickly what kernel you are running under -- just check whether EBX or ECX are zeroes on startup:
test ebx,ebx jz .kernel22 ;it is Linux 2.2 .kernel20: ;otherwise it is Linux 2.0 ... .kernel22: ...Also, you probably noticed from regs output that FS and GS are not used in Linux 2.2; and they are no longer present in pt_regs structure..