// The goal of this log is to _fully understand_ every effect of every CPU instruction // and observe them in GDB, using commands that show registers, show memory, and step // forward one instruction ("stepi" or "si"). // // Work through every instruction in this program, call up its definitions and // explanations in your favorite AI assistant if necessary. // [sergey@thepond cs59]$ gdb ./hello GNU gdb (GDB) 17.2 [skipped] (No debugging symbols found in ./hello) // Note that we have not yet started the process or even loaded the file into RAM. // Disassembly goes by offsets of instructions into the executable file. // The number in <> is relative offset from the start of the function. It is handy // for seeing the targets of jumps. (gdb) disas main Dump of assembler code for function main: 0x0000000000001139 <+0>: push %rbp 0x000000000000113a <+1>: mov %rsp,%rbp 0x000000000000113d <+4>: sub $0x10,%rsp 0x0000000000001141 <+8>: movl $0xa,-0x4(%rbp) 0x0000000000001148 <+15>: jmp 0x115d 0x000000000000114a <+17>: subl $0x1,-0x4(%rbp) 0x000000000000114e <+21>: lea 0xeaf(%rip),%rax # 0x2004 0x0000000000001155 <+28>: mov %rax,%rdi 0x0000000000001158 <+31>: call 0x1030 0x000000000000115d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000000000001161 <+40>: jns 0x114a 0x0000000000001163 <+42>: mov $0x2a,%eax 0x0000000000001168 <+47>: leave 0x0000000000001169 <+48>: ret End of assembler dump. // Let's see the raw bytes of instructions. Note that they define the next offsets, // because the instructions are packed together without breaks: (gdb) disas /r main Dump of assembler code for function main: 0x0000000000001139 <+0>: 55 push %rbp 0x000000000000113a <+1>: 48 89 e5 mov %rsp,%rbp 0x000000000000113d <+4>: 48 83 ec 10 sub $0x10,%rsp 0x0000000000001141 <+8>: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) 0x0000000000001148 <+15>: eb 13 jmp 0x115d 0x000000000000114a <+17>: 83 6d fc 01 subl $0x1,-0x4(%rbp) 0x000000000000114e <+21>: 48 8d 05 af 0e 00 00 lea 0xeaf(%rip),%rax # 0x2004 0x0000000000001155 <+28>: 48 89 c7 mov %rax,%rdi 0x0000000000001158 <+31>: e8 d3 fe ff ff call 0x1030 0x000000000000115d <+36>: 83 7d fc 00 cmpl $0x0,-0x4(%rbp) 0x0000000000001161 <+40>: 79 e7 jns 0x114a 0x0000000000001163 <+42>: b8 2a 00 00 00 mov $0x2a,%eax 0x0000000000001168 <+47>: c9 leave 0x0000000000001169 <+48>: c3 ret End of assembler dump. // Look above and ask an AI to explain the JNS instruction. e7 is an offset of the jump's // target. It is encoded into the instruction as one byte representing a negative offset // of -25 as a signed 8-bit two's complement number. // Note how the target of JNS jump is computed: the next instruction after JNS is at // the offset 42 from the top of the function, so 42-25 = 17, which is exacltly the target. // x86 encoding is nice this way, you see the offsets and constants ("immediates") right away. // Stop and read about two's complement signed arithmetic if the concept is new to you! (gdb) [1]+ Stopped gdb ./hello [sergey@thepond cs59]$ xxd hello | less [sergey@thepond cs59]$ xxd hello | head 00000000: 7f45 4c46 0201 0100 0000 0000 0000 0000 .ELF............ // <<-- magic bytes 00000010: 0300 3e00 0100 0000 4010 0000 0000 0000 ..>.....@....... // <<-- metadata about 00000020: 4000 0000 0000 0000 a836 0000 0000 0000 @........6...... // this ELF file's 00000030: 0000 0000 4000 3800 0f00 4000 1f00 1e00 ....@.8...@..... // ISA, encoding, etc. 00000040: 0600 0000 0400 0000 4000 0000 0000 0000 ........@....... 00000050: 4000 0000 0000 0000 4000 0000 0000 0000 @.......@....... 00000060: 4803 0000 0000 0000 4803 0000 0000 0000 H.......H....... 00000070: 0800 0000 0000 0000 0300 0000 0400 0000 ................ 00000080: ac03 0000 0000 0000 ac03 0000 0000 0000 ................ 00000090: ac03 0000 0000 0000 1c00 0000 0000 0000 ................ [sergey@thepond cs59]$ readelf -a hello | less [sergey@thepond cs59]$ readelf -a hello | head -n 15 ELF Header: Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00 Class: ELF64 Data: 2's complement, little endian // <<-- encoding of integers Version: 1 (current) OS/ABI: UNIX - System V ABI Version: 0 Type: DYN (Position-Independent Executable file) Machine: Advanced Micro Devices X86-64 // <<-- ISA of CPU Version: 0x1 Entry point address: 0x1040 // <<-- offset of first executable instruction Start of program headers: 64 (bytes into file) // <<-- offsets of various metadata tables Start of section headers: 13992 (bytes into file) Flags: 0x0 Size of this header: 64 (bytes) // Back to the debugger [sergey@thepond cs59]$ fg gdb ./hello Dump of assembler code for function main: 0x0000000000001139 <+0>: 55 push %rbp 0x000000000000113a <+1>: 48 89 e5 mov %rsp,%rbp 0x000000000000113d <+4>: 48 83 ec 10 sub $0x10,%rsp 0x0000000000001141 <+8>: c7 45 fc 0a 00 00 00 movl $0xa,-0x4(%rbp) 0x0000000000001148 <+15>: eb 13 jmp 0x115d 0x000000000000114a <+17>: 83 6d fc 01 subl $0x1,-0x4(%rbp) 0x000000000000114e <+21>: 48 8d 05 af 0e 00 00 lea 0xeaf(%rip),%rax # 0x2004 0x0000000000001155 <+28>: 48 89 c7 mov %rax,%rdi 0x0000000000001158 <+31>: e8 d3 fe ff ff call 0x1030 0x000000000000115d <+36>: 83 7d fc 00 cmpl $0x0,-0x4(%rbp) 0x0000000000001161 <+40>: 79 e7 jns 0x114a 0x0000000000001163 <+42>: b8 2a 00 00 00 mov $0x2a,%eax 0x0000000000001168 <+47>: c9 leave 0x0000000000001169 <+48>: c3 ret End of assembler dump. // Let's see the first instruction at the entry point: (gdb) x/10i 0x1040 0x1040 <_start>: endbr64 0x1044 <_start+4>: xor %ebp,%ebp // <<--- this zeros out EBP 0x1046 <_start+6>: mov %rdx,%r9 0x1049 <_start+9>: pop %rsi 0x104a <_start+10>: mov %rsp,%rdx 0x104d <_start+13>: and $0xfffffffffffffff0,%rsp // <-- this forces RSP to be 16-byte aligned, by zeroing out its last 4 bits. x86 likes alignment for memory efficiency. 0x1051 <_start+17>: push %rax 0x1052 <_start+18>: push %rsp 0x1053 <_start+19>: xor %r8d,%r8d 0x1056 <_start+22>: xor %ecx,%ecx (gdb) x/20i 0x1040 0x1040 <_start>: endbr64 0x1044 <_start+4>: xor %ebp,%ebp 0x1046 <_start+6>: mov %rdx,%r9 0x1049 <_start+9>: pop %rsi 0x104a <_start+10>: mov %rsp,%rdx 0x104d <_start+13>: and $0xfffffffffffffff0,%rsp 0x1051 <_start+17>: push %rax 0x1052 <_start+18>: push %rsp 0x1053 <_start+19>: xor %r8d,%r8d 0x1056 <_start+22>: xor %ecx,%ecx 0x1058 <_start+24>: lea 0xda(%rip),%rdi # 0x1139
// <<-- this loads main()'s address into RDI 0x105f <_start+31>: call *0x2f5b(%rip) # 0x3fc0 // <<-- this will eventually call main() 0x1065 <_start+37>: hlt [skipped] // See the explanation of the LEA instruction. It computes the address and loads into into // its target register. Recall that addressing in x86-64 are typically RIP-relative. // See the encoding of this instruction. Can you find and interpret the immediate/offset? // Now let us actually load and run the program "hello" so that we see real memory addresses // and contents, not just file offsets and contents. (gdb) b main Breakpoint 1 at 0x1141 (gdb) run Starting program: /home/sergey/cs59/hello [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". Breakpoint 1, 0x0000555555555141 in main () // Now we are looking at the (virtual) addresses in RAM where our code is loaded and runs! (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp // ask what this does! Saves caller function's EBP. 0x000055555555513a <+1>: mov %rsp,%rbp // Our RBP is now RSP-8 0x000055555555513d <+4>: sub $0x10,%rsp // <<-- our "stack frame" will be 16 bytes // Our stack frame is all addressed via offsets from RBP => 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) // <<-- this is our local variable "i" 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // We landed just past the standard x86 function preamble, created by the compiler // for every function. The goal of the preamble is to save the RBP of the currently // executing function, then put the current RSP's value into RBP, and then move RSP // to make room for the function's own local variables. All of these local variables // will be addressed as (negative) offsets off of this RBP, which will not change // during this function. // All of this is a _convention_ of how C code is compiled. The CPU doesn't know or care. // Addresses held in CPU registers are indistinguishable from integers indistinguishable // from offsets. // You can play with the GDB TUI, see https://www.youtube.com/watch?app=desktop&v=PorfLSr3DDI // or https://dev.to/irby/making-gdb-easier-the-tui-interface-15l2 (gdb) tui enable // Let's see the "stack frame". Our frame is the 16 zero bytes. It just so happened to be 0s, // we get whatever was there before. (gdb) x/8g $rsp 0x7fffffffea60: 0x0000000000000000 0x0000000000000000 0x7fffffffea70: 0x00007fffffffeb20 0x00007ffff7c27781 // Saved EBP. Saved RIP to return to after main() is done and hits its RET (see below). 0x7fffffffea80: 0x00007ffff7fe0ce0 0x00007fffffffeba8 0x7fffffffea90: 0x00000001f7fbe000 0x0000555555555139 // We see the address of main(). What put it there? Look down from _start to find out. // Let's see the registers: (gdb) print $rbp $1 = (void *) 0x7fffffffea70 (gdb) print $rsp $2 = (void *) 0x7fffffffea60 // Memory seen as characters: (gdb) x/16c $rsp 0x7fffffffea60: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'0 '\000' 0 '\000' 0 '\000' 0x7fffffffea68: 0 '\000' 0 '\000' 0 '\000' 0 '\000' 0 '\000'0 '\000' 0 '\000' 0 '\000' // ... and as bytes in hex (gdb) x/16x $rsp 0x7fffffffea60: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x7fffffffea68: 0x00 0x00 0x00 0x00 0x00 0x00 0x00 0x00 // We step one instruction, the MOVL that writes i's value (0x0a) into our stack frame: (gdb) si 0x0000555555555148 in main () (gdb) x/8g $rsp 0x7fffffffea60: 0x0000000000000000 0x0000000a00000000 <<-- 4 bytes (int), little-endian 0x7fffffffea70: 0x00007fffffffeb20 0x00007ffff7c27781 0x7fffffffea80: 0x00007ffff7fe0ce0 0x00007fffffffeba8 0x7fffffffea90: 0x00000001f7fbe000 0x0000555555555139 // OK, I have muscle memory from my 32 bit days when SP was ESP (not RSP):. (gdb) x/1w $ebp-4 0xffffffffffffea6c: Cannot access memory at address 0xffffffffffffea6c // This 4-byte chunk of memory seen as an int (gdb) x/1w $rbp-4 0x7fffffffea6c: 0x0000000a // ...and as 4 bytes in hex: (gdb) x/4b $rbp-4 0x7fffffffea6c: 0x0a 0x00 0x00 0x00 (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) => 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. (gdb) si 0x000055555555515d in main () (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 => 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // CMP only sets the flags (ask an AI for an explanation) (gdb) print $eflags $3 = [ PF IF ] (gdb) si 0x0000555555555161 in main () (gdb) print $eflags $4 = [ PF IF ] (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) => 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // Oops, GDB needs a * before an address, otherwise it assumes it must be a function name. // It's perfectly willing to set a breakpoint on the function _by name_, should it ever get loaded. But that's not what we want, we want a breakpoint at the specific instruction. (gdb) b 0x000055555555515d Function "0x000055555555515d" not defined. Make breakpoint pending on future shared library load? (y or [n]) n // Now we have it: (gdb) b *0x000055555555515d Breakpoint 2 at 0x55555555515d (gdb) si 0x000055555555514a in main () (gdb) info b Num Type Disp Enb Address What 1 breakpoint keep y 0x0000555555555141 breakpoint already hit 1 time 2 breakpoint keep y 0x000055555555515d (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d => 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // We are about to subtract 1 from our "i", in memory. This is an x86-ism. // ARM doesn't allow this, on ARM you must load a value into a register to do arithmetic on it. (gdb) x/4b $rbp-4 0x7fffffffea6c: 0x0a 0x00 0x00 0x00 (gdb) si 0x000055555555514e in main () // ... and i is now 9: (gdb) x/4b $rbp-4 0x7fffffffea6c: 0x09 0x00 0x00 0x00 // We are about to do another LEA, for the string to pass to puts(): (gdb) x/10b 0x555555556004 0x555555556004: 0x48 0x65 0x6c 0x6c 0x6f 0x00 0x00 0x00 0x55555555600c: 0x01 0x1b // This is really just the address of the "Hello\0" string. (gdb) x/s 0x555555556004 0x555555556004: "Hello" (gdb) print $rax $5 = 140737352175080 (gdb) si 0x0000555555555155 in main () // It is now in RAX: (gdb) i r $rax rax 0x555555556004 93824992239620 (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 => 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // I somehow allowed the program to exit without cathing the last iteration of the loop. // So now I rerun in. Counting is hard! (gdb) r Starting program: /home/sergey/cs59/hello [Thread debugging using libthread_db enabled] Using host libthread_db library "/usr/lib/libthread_db.so.1". Breakpoint 1, 0x0000555555555141 in main () (gdb) c Continuing. Breakpoint 2, 0x000055555555515d in main () (gdb) c Continuing. Hello Breakpoint 2, 0x000055555555515d in main () (gdb) c Continuing. Hello Breakpoint 2, 0x000055555555515d in main () (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 => 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. (gdb) delete Delete all breakpoints, watchpoints, tracepoints, and catchpoints? (y or n) y // Better idea: set a breakpoint right after JNS falls through the end of the loop, to MOV (gdb) b main Breakpoint 4 at 0x555555555141 (gdb) b *0x0000555555555163 Breakpoint 5 at 0x555555555163 (gdb) c Continuing. Hello Hello Hello Hello Hello Hello Hello Hello Hello Breakpoint 5, 0x0000555555555163 in main () // Aha! Our "i" is 0-1, i.e., -1 in the 32-bit two's complement representation. (gdb) x/1wx $rbp-4 0x7fffffffea6c: 0xffffffff // Its top bit is 1, a.k.a. the Sign Flag, SF. We can see it was set in EFLAGS, // which is what JNS picked up on and fell through instead of jumping to main+17 as // it did all the previous times: (gdb) print $eflags $9 = [ PF SF IF ] // <<-- behold the sign flag (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a => 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. // By x86-64 calling convention, a function's integer return value is put into EAX // or RAX if a pointer/address or a 64-bit integer (the CPU doesn't know or care). (gdb) print $eax $10 = 6 // .. what is this 6? We don't know. Maybe it's what puts() returned. You can find out :) // And now we put out return value 42 (0x2a) there: (gdb) si 0x0000555555555168 in main () (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax => 0x0000555555555168 <+47>: leave 0x0000555555555169 <+48>: ret End of assembler dump. (gdb) print $eax $11 = 42 // We are about to LEAVE. See what that instruction does. It undoes the effects of the // preamble. You cna think of it as letting go of the stack frame, except the memory // contents are still all there, it's just that RBP and RSP no longer point to them. (gdb) print $rbp $13 = (void *) 0x7fffffffea70 (gdb) print $rsp $14 = (void *) 0x7fffffffea60 // This is the RBP we saved with preamble's PUSH on entry to this function. We'll // restore it back into RBP with LEAVE: (gdb) x/1gx $rbp 0x7fffffffea70: 0x00007fffffffeb20 (gdb) si 0x0000555555555169 in main () // .. and we did it: (gdb) print $rbp $15 = (void *) 0x7fffffffeb20 // .. and RSP is now pointing past it. In fact, it's pointing right at the address to // go back to, which RET will pick off the stack and put into RIP: (gdb) print $rsp $16 = (void *) 0x7fffffffea78 (gdb) x/1gx $rsp 0x7fffffffea78: 0x00007ffff7c27781 (gdb) x/2gx $rsp 0x7fffffffea78: 0x00007ffff7c27781 0x00007ffff7fe0ce0 (gdb) x/3gx $rsp 0x7fffffffea78: 0x00007ffff7c27781 0x00007ffff7fe0ce0 0x7fffffffea88: 0x00007fffffffeba8 (gdb) x/4gx $rsp 0x7fffffffea78: 0x00007ffff7c27781 0x00007ffff7fe0ce0 0x7fffffffea88: 0x00007fffffffeba8 0x00000001f7fbe000 (gdb) x/8gx $rsp 0x7fffffffea78: 0x00007ffff7c27781 0x00007ffff7fe0ce0 0x7fffffffea88: 0x00007fffffffeba8 0x00000001f7fbe000 0x7fffffffea98: 0x0000555555555139 0x00007fffffffeae0 0x7fffffffeaa8: 0x00007ffff7fdf2e0 0x0000000000000000 // We are about to RET: (gdb) disas main Dump of assembler code for function main: 0x0000555555555139 <+0>: push %rbp 0x000055555555513a <+1>: mov %rsp,%rbp 0x000055555555513d <+4>: sub $0x10,%rsp 0x0000555555555141 <+8>: movl $0xa,-0x4(%rbp) 0x0000555555555148 <+15>: jmp 0x55555555515d 0x000055555555514a <+17>: subl $0x1,-0x4(%rbp) 0x000055555555514e <+21>: lea 0xeaf(%rip),%rax # 0x555555556004 0x0000555555555155 <+28>: mov %rax,%rdi 0x0000555555555158 <+31>: call 0x555555555030 0x000055555555515d <+36>: cmpl $0x0,-0x4(%rbp) 0x0000555555555161 <+40>: jns 0x55555555514a 0x0000555555555163 <+42>: mov $0x2a,%eax 0x0000555555555168 <+47>: leave => 0x0000555555555169 <+48>: ret End of assembler dump. (gdb) print $rip $17 = (void (*)()) 0x555555555169 (gdb) si 0x00007ffff7c27781 in ?? () from /usr/lib/libc.so.6 // And we picked up an address from the stack (at RSP as restored by LEAVE) and put it into RIP: (gdb) print $rip $18 = (void (*)()) 0x7ffff7c27781 // This lands up into the function that called main(), just after it called main(). /// It will now call exit(), which will eventually hand our exit code 42 to the Unix shell. (gdb) x/10i $rip => 0x7ffff7c27781: mov %eax,%edi 0x7ffff7c27783: call 0x7ffff7c41080 0x7ffff7c27788: call 0x7ffff7c95340 0x7ffff7c2778d: lea -0x78(%rbp),%rdx 0x7ffff7c27791: xor %edi,%edi 0x7ffff7c27793: mov $0xe,%eax 0x7ffff7c27798: mov $0x8,%r10d 0x7ffff7c2779e: lea 0x19ac0b(%rip),%rsi # 0x7ffff7dc23b0 0x7ffff7c277a5: syscall 0x7ffff7c277a7: xor %r10d,%r10d (gdb) // Now, can you see the instruction that called main()? It it located just before 0x7ffff7c27781, // but since instructions in x86 are variable length, you need to be careful. Disassembling // with "x/i" will give you answers, but you need to know where the instruction really starts, // otherwise the answers/disassembly will be non-sensical. // Do it and see! You'll know you got the instruction length right when "x/10i" at RIP minus // that length will give you valid-looking instructions that align with the above disassembly. // Wrong lengths/offsets will give you "(bad)" instructions that don't decode. // There is a lot more to the standard library's execution path from the entry point (_start) // to main(). Beware of the rabbithole: // https://0xax.gitbooks.io/linux-insides/content/Misc/linux-misc-4.html // https://iq.thc.org/how-does-linux-start-a-process // https://gist.github.com/gagomes/a9e338162eab7553c5d648aa6210b882 // and so on :)