Before going to through the demos, we will use the demo archive. Demos are to be run on a Linux system. We will download the archive using
wget http://elf.cs.pub.ro/cns/res/lectures/lecture-02-demo.zip
and then unpack it
unzip lecture-02-demo.zip
and access the unpacked folder
cd lecture-02-demo/
We can now go through the demos. Assuming we don't know anything about calling conventions and syscall conventions for x86/x86_64 we want to document how they are carried out on linux ELF executables. We'll only deal with 4 arguments initially
Let's compile and disassemble the relevant parts:
$ gcc -Wall demo1.c -m32 -o demo1_32 $ objdump -d demo1_32 -Mintel -w [...] 08048423 <main>: 8048423: 55 push ebp 8048424: 89 e5 mov ebp,esp 8048426: 83 ec 10 sub esp,0x10 8048429: c7 44 24 0c 03 00 00 00 mov DWORD PTR [esp+0xc],0x3 8048431: c7 44 24 08 02 00 00 00 mov DWORD PTR [esp+0x8],0x2 8048439: c7 44 24 04 01 00 00 00 mov DWORD PTR [esp+0x4],0x1 8048441: c7 04 24 00 00 00 00 mov DWORD PTR [esp],0x0 8048448: e8 bf ff ff ff call 804840c <testfunc> 804844d: c9 leave 804844e: c3 ret 804844f: 90 nop [...]
So when testfunc
is called the stack will look as follows:
[esp+0x00] ret addr (pushed because of 'call') [esp+0x04] 0 [esp+0x08] 1 [esp+0x0c] 2 [esp+0x10] 3
Another important aspect in calling conventions is the return value:
[...] 0804840c <testfunc>: 804840c: 55 push ebp 804840d: 89 e5 mov ebp,esp 804840f: 8b 45 0c mov eax,DWORD PTR [ebp+0xc] 8048412: 8b 55 08 mov edx,DWORD PTR [ebp+0x8] 8048415: 01 c2 add edx,eax 8048417: 8b 45 10 mov eax,DWORD PTR [ebp+0x10] 804841a: 01 c2 add edx,eax 804841c: 8b 45 14 mov eax,DWORD PTR [ebp+0x14] 804841f: 01 d0 add eax,edx 8048421: 5d pop ebp 8048422: c3 ret [...]
As we can see, all the values are added into eax
. We conclude that eax
holds return values
$ gcc -Wall demo1.c -m64 -o demo1_64 $ objdump -d demo1_64 -Mintel -w 0000000000400540 <main>: 400540: 55 push rbp 400541: 48 89 e5 mov rbp,rsp 400544: b9 03 00 00 00 mov ecx,0x3 400549: ba 02 00 00 00 mov edx,0x2 40054e: be 01 00 00 00 mov esi,0x1 400553: bf 00 00 00 00 mov edi,0x0 400558: e8 bf ff ff ff call 40051c <testfunc> 40055d: 5d pop rbp 40055e: c3 ret 40055f: 90 nop
The arguments are passed, respectively, using rdi
, rsi
, rdx
, rcx
000000000040051c <testfunc>: 40051c: 55 push rbp 40051d: 48 89 e5 mov rbp,rsp 400520: 89 7d fc mov DWORD PTR [rbp-0x4],edi 400523: 89 75 f8 mov DWORD PTR [rbp-0x8],esi 400526: 89 55 f4 mov DWORD PTR [rbp-0xc],edx 400529: 89 4d f0 mov DWORD PTR [rbp-0x10],ecx 40052c: 8b 45 f8 mov eax,DWORD PTR [rbp-0x8] 40052f: 8b 55 fc mov edx,DWORD PTR [rbp-0x4] 400532: 01 c2 add edx,eax 400534: 8b 45 f4 mov eax,DWORD PTR [rbp-0xc] 400537: 01 c2 add edx,eax 400539: 8b 45 f0 mov eax,DWORD PTR [rbp-0x10] 40053c: 01 d0 add eax,edx 40053e: 5d pop rbp 40053f: c3 ret
The return value is as before in rax
As you can imagine, functions with more parameters will start to use the stack when the number of registers runs out. Try it yourself and find out when this happens.
Doing the same thing for syscalls is a bit trickier. We would like to use an architecture-independent approach. To do that we can't rely on hardcoded assembly (as it defeats our purpose anyway).
Instead, we'll use the syscall function provided by libc
(man 2 syscall
):
Our example simply writes “Hello World” to stderr.
Unfortunately, objdump on the binary doesn't help us too much:
$ gcc -Wall demo2.c -m32 -o demo2_32 $ objdump -d demo2_32 -Mintel -w 0804843c <main>: 804843c: 55 push ebp 804843d: 89 e5 mov ebp,esp 804843f: 83 e4 f0 and esp,0xfffffff0 8048442: 83 ec 10 sub esp,0x10 8048445: c7 44 24 0c 0d 00 00 mov DWORD PTR [esp+0xc],0xd 804844c: 00 804844d: c7 44 24 08 00 85 04 mov DWORD PTR [esp+0x8],0x8048500 8048454: 08 8048455: c7 44 24 04 02 00 00 mov DWORD PTR [esp+0x4],0x2 804845c: 00 804845d: c7 04 24 04 00 00 00 mov DWORD PTR [esp],0x4 8048464: e8 c7 fe ff ff call 8048330 <syscall@plt> 8048469: b8 00 00 00 00 mov eax,0x0 804846e: c9 leave 804846f: c3 ret
We only see an opaque call to syscall in libc. But libc can also be inspected with objdump to get the source code of syscall()
$ objdump -d /lib32/libc.so.6 -Mintel -w [...] 000f0e50 <syscall>: f0e50: 55 push ebp f0e51: 57 push edi f0e52: 56 push esi f0e53: 53 push ebx f0e54: 8b 6c 24 2c mov ebp,DWORD PTR [esp+0x2c] f0e58: 8b 7c 24 28 mov edi,DWORD PTR [esp+0x28] f0e5c: 8b 74 24 24 mov esi,DWORD PTR [esp+0x24] f0e60: 8b 54 24 20 mov edx,DWORD PTR [esp+0x20] f0e64: 8b 4c 24 1c mov ecx,DWORD PTR [esp+0x1c] f0e68: 8b 5c 24 18 mov ebx,DWORD PTR [esp+0x18] f0e6c: 8b 44 24 14 mov eax,DWORD PTR [esp+0x14] f0e70: 65 ff 15 10 00 00 00 call DWORD PTR gs:0x10 f0e77: 5b pop ebx f0e78: 5e pop esi f0e79: 5f pop edi f0e7a: 5d pop ebp f0e7b: 3d 01 f0 ff ff cmp eax,0xfffff001 f0e80: 73 01 jae f0e83 <syscall+0x33> f0e82: c3 ret [...]
call DWORD PTR gs:0x10
is an optimized equivalent of int 0x80
The stack at the first instruction in syscall is
[esp+0x00] ret addr (pushed because of 'call') [esp+0x04] 4 (__NR_write) [esp+0x08] 2 [esp+0x0c] addr of "Hello World!" [esp+0x10] 12
After 4 pushes we have
[esp+0x00] ebx [esp+0x04] esi [esp+0x08] edi [esp+0x0c] ebp [esp+0x10] ret addr (pushed because of 'call') [esp+0x14] 4 (__NR_write) [esp+0x18] 2 [esp+0x1c] addr of "Hello World!" [esp+0x20] 12
We can correlate this with the register set up before call DWORD PTR gs:0x10
to get the full picture:
eax
holds the syscall numberebx
holds argument 1ecx
holds argument 2edx
holds argument 3esi
holds argument 4and so on
Doing the same for 64 bits we see the following disassembly of syscall()
$ gcc -Wall demo2.c -m64 -o demo2_64 $ objdump -d demo2_64 -Mintel -w 00000000000e90c0 <syscall>: e90c0: 48 89 f8 mov rax,rdi e90c3: 48 89 f7 mov rdi,rsi e90c6: 48 89 d6 mov rsi,rdx e90c9: 48 89 ca mov rdx,rcx e90cc: 4d 89 c2 mov r10,r8 e90cf: 4d 89 c8 mov r8,r9 e90d2: 4c 8b 4c 24 08 mov r9,QWORD PTR [rsp+0x8] e90d7: 0f 05 syscall e90d9: 48 3d 01 f0 ff ff cmp rax,0xfffffffffffff001 e90df: 73 01 jae e90e2 <syscall+0x22> e90e1: c3 ret
rax
holds the syscall numberrdi
holds argument 1rsi
holds argument 2rdx
holds argument 3r10
holds argument 4