1

I have this simple C program in a file named test.c :

void fx2(){

    int c = 30;
    c++;
}

void fx1(){

    int b = 20;
    b++;
    fx2();

}

int main(){

    int a = 10;
    a++; 
    fx1();
}

I've compiled this in an Ubuntu ( 22.04.3 ) x86_64 system by using "gcc test.c -o test". Then I used "objdump -d test" and I got this :

0000000000001129 <fx2>:
    1129:   f3 0f 1e fa             endbr64 
    112d:   55                      push   %rbp
    112e:   48 89 e5                mov    %rsp,%rbp
    1131:   c7 45 fc 1e 00 00 00    movl   $0x1e,-0x4(%rbp)
    1138:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
    113c:   90                      nop
    113d:   5d                      pop    %rbp
    113e:   c3                      ret    

000000000000113f <fx1>:
    113f:   f3 0f 1e fa             endbr64 
    1143:   55                      push   %rbp
    1144:   48 89 e5                mov    %rsp,%rbp
    1147:   48 83 ec 10             sub    $0x10,%rsp
    114b:   c7 45 fc 14 00 00 00    movl   $0x14,-0x4(%rbp)
    1152:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
    1156:   b8 00 00 00 00          mov    $0x0,%eax
    115b:   e8 c9 ff ff ff          call   1129 <fx2>
    1160:   90                      nop
    1161:   c9                      leave  
    1162:   c3                      ret    

0000000000001163 <main>:
    1163:   f3 0f 1e fa             endbr64 
    1167:   55                      push   %rbp
    1168:   48 89 e5                mov    %rsp,%rbp
    116b:   48 83 ec 10             sub    $0x10,%rsp
    116f:   c7 45 fc 0a 00 00 00    movl   $0xa,-0x4(%rbp)
    1176:   83 45 fc 01             addl   $0x1,-0x4(%rbp)
    117a:   b8 00 00 00 00          mov    $0x0,%eax
    117f:   e8 bb ff ff ff          call   113f <fx1>
    1184:   b8 00 00 00 00          mov    $0x0,%eax
    1189:   c9                      leave  
    118a:   c3                      ret    

I've also compiled test.c in a macOS ( Monteray 12.6.5 ) x86_64 system with "clang test.c -o test". Then I used "otool -tV test" and I got this :

(__TEXT,__text) section
_fx2:
0000000100003f40    pushq   %rbp
0000000100003f41    movq    %rsp, %rbp
0000000100003f44    movl    $0x1e, -0x4(%rbp)
0000000100003f4b    movl    -0x4(%rbp), %eax
0000000100003f4e    addl    $0x1, %eax
0000000100003f51    movl    %eax, -0x4(%rbp)
0000000100003f54    popq    %rbp
0000000100003f55    retq
0000000100003f56    nopw    %cs:(%rax,%rax)
_fx1:
0000000100003f60    pushq   %rbp
0000000100003f61    movq    %rsp, %rbp
0000000100003f64    subq    $0x10, %rsp
0000000100003f68    movl    $0x14, -0x4(%rbp)
0000000100003f6f    movl    -0x4(%rbp), %eax
0000000100003f72    addl    $0x1, %eax
0000000100003f75    movl    %eax, -0x4(%rbp)
0000000100003f78    callq   _fx2
0000000100003f7d    addq    $0x10, %rsp
0000000100003f81    popq    %rbp
0000000100003f82    retq
0000000100003f83    nopw    %cs:(%rax,%rax)
0000000100003f8d    nopl    (%rax)
_main:
0000000100003f90    pushq   %rbp
0000000100003f91    movq    %rsp, %rbp
0000000100003f94    subq    $0x10, %rsp
0000000100003f98    movl    $0xa, -0x4(%rbp)
0000000100003f9f    movl    -0x4(%rbp), %eax
0000000100003fa2    addl    $0x1, %eax
0000000100003fa5    movl    %eax, -0x4(%rbp)
0000000100003fa8    callq   _fx1
0000000100003fad    xorl    %eax, %eax
0000000100003faf    addq    $0x10, %rsp
0000000100003fb3    popq    %rbp
0000000100003fb4    retq

Here's my questions:

  1. Considering this simple C program, can I be 100% sure that otool and objdump show the exact machine instructions contained in the executable "test" ? For example, are the two function prologue instructions pushq %rbp and movq %rsp,%rbp really contained in the actual executable "test" or can these dis-assemblers omit/add/modify instructions to make the program easier to read and follow ?

  2. It looks like gcc and clang in Ubuntu and macOS respectively do use the frame pointers. Can we say that Ubuntu and macOS systems do use frame pointers or would it be more appropriate to say that only the specific compilers gcc and clang in the context of Ubuntu and macOS respectively use the frame pointer ?

Sep Roland
  • 33,889
  • 7
  • 43
  • 76
alessio solari
  • 313
  • 1
  • 6
  • 4
    'or these disassemblers can omit/add/modify instructions' - that would make the disassemblers worse than useless :) – Martin James Aug 15 '23 at 10:40

2 Answers2

4

Considering this simple C program can I be 100% sure that otool and objdump show the exact machine instructions contained in the executable test ? For example are the two function prologue instructions pushq %rbp and movq %rsp,%rbp" really contained in the actual executuable "test" or these disassemblers can omit/add/modify instructions to make the program easier to read and follow ?

They cannot always show the “exact” machine instructions since relocations may occur when the executable is assigned addresses in memory and addresses in the instruction stream are updated as the program is loaded.

Aside from that, they should show the actual instructions in the executable, aside from using different mnemonics or variations in display that have the same meaning, possibly including using mnemonics for multiple instructions.

Looks like gcc and clang in Ubuntu and macOS respectively do use the frame pointers. Can we say that Ubuntu and macOS systems do use frame pointers or it would be more appropriate to say that only the specific compilers gcc and clang in the context of Ubuntu and macOS respectively use the frame pointer ?

Both Clang and GCC have a switch not to use a frame pointer, -fomit-frame-pointer, so clearly they do not always use a frame pointer. From experience using this in high-performance code, the primary effect of not using the frame pointer was that it disrupts debugging, since a debugger, at least in the macOS environment, relied on the frame pointer for some information about routines further up in the call tree.

Eric Postpischil
  • 195,579
  • 13
  • 168
  • 312
  • what do you mean by "They cannot always show the “exact” machine instructions since relocations may occur when the executable is assigned addresses in memory and addresses in the instruction stream are updated as the program is loaded". Are you talking about virtual-physical memory mapping ? – alessio solari Aug 15 '23 at 12:40
  • 1
    @alessiosolari: No, it is not about physical memory. Virtual memory addresses might not be assigned until a program is loaded. For example, on one version of macOS, `otool` shows the call instruction for `printf("%p\n", (void *) &printf);` as `callq 0x100000f8e`, but running the program shows the address as `0x7fff6522cec4`. The `0x100000f8e` that `otool` shows is just a placeholder for what the final address will be. `otool` cannot know the final address because it has not been assigned in the executable file; it is only assigned when the program is loaded. – Eric Postpischil Aug 15 '23 at 12:51
  • so basically the virtual addresses displayed by disassemblers are not definitive ? For example if I take the first instruction in my C compiled program "0000000100003f90 pushq %rbp" I have no guarantee that the virtual address of this instruction will be 0000000100003f90 ? Can it be changed ? – alessio solari Aug 15 '23 at 13:16
  • @alessiosolari: Right, macOS executables, and PIEs on Linux, can do ASLR, randomizing the base address of all the sections by the same amount so their relative positions stay the same so relative addressing still works. Linux non-PIE executables (`gcc -fno-pie -no-pie foo.c`) can't; the linker chooses an address and the OS must run it at that (virtual) address. So the executable can contain 32-bit absolute addresses for its own code and data without even having relocation entries for them. – Peter Cordes Aug 15 '23 at 16:17
  • [32-bit absolute addresses no longer allowed in x86-64 Linux?](https://stackoverflow.com/q/43367427) - most Linux distros configure GCC so `-fPIE -pie` is the default now, changing what you can do in hand-written asm unless you override that. – Peter Cordes Aug 15 '23 at 16:17
  • *possibly including using mnemonics for multiple instructions.* - Just for the record, no mainstream x86 disassemblers do that. Some RISC-V disassemblers might combine `lui reg, imm20` / `addi reg, reg, imm12` into an `li` pseudo-instruction to load a 32-bit constant, or maybe even an equivalent for AArch64 mov/movk, but x86 with it's variable-width instructions can just put a whole 32 or 64-bit constant in one wide instruction with an opcode. Also, even for RISC ISAs, most disassemblers don't collapse / combine into pseudo-instructions by default, especially ones that show hex machine-code. – Peter Cordes Aug 15 '23 at 18:30
  • @EricPostpischil, https://stackoverflow.com/questions/76915234/function-address-in-executable-inspected-by-objdump-not-matching-the-address?noredirect=1#comment135592601_76915234 – alessio solari Aug 16 '23 at 16:32
2
  1. Disassemblers typically emit exactly the instructions found in the executable. They may have some trouble where binary data is interspersed with instructions in a way that doesn't confuse the processor but may make it hard for the disassembler to decide where code continues. For example, the clang compiler seems to generate "nonsense" bytes as padding so that the next function starts on a 16-byte boundary which has advantages for cache behavior. The disassembler doesn't know this and tries to interpret them as instructions.
  2. Use of frame pointers (and which registers are used for parameter and return value passing) is part of an ABI or calling convention which is often given for a platform (operating system) and followed by the compilers for that platform. Some languages may use different calling conventions internally but use the standard platform convention when interfacing with C library code.
  • 1
    Both Linux and macOS use the x86-64 System V ABI, which does *not* require the use of RBP as a frame pointer. The OP used the default optimization level (`-O0`), which implies `-fno-omit-frame-pointer`, so compilers chose to do that. Even in calling conventions that require a frame pointer, that's only for stack unwinding on exceptions or by debuggers; stack args are at a fixed location (on function entry) relative to the *stack* pointer. The callee has to set up its own frame pointer, not use the caller's, so code with/without `-fomit-frame-pointer` can interoperate. – Peter Cordes Aug 15 '23 at 16:12
  • 1
    GCC and clang pad between functions with long `nop`s, despite the fact that code will never execute. It's not "nonsense" like `00` bytes that could be the start of a longer instruction and break disassembly of the next function. (Fun fact: MSVC pads between functions with `0xcc` `int3` software breakpoint, NOPs inside functions.) – Peter Cordes Aug 15 '23 at 16:20
  • 1
    But yes, an obfuscated executable could disassemble weird, like if data was mixed in with code, or a jump jumped backwards into the middle of byte that had previously executed as a different instruction (e.g. a `test eax, imm32` before a loop to skip the first 4 bytes on the first iteration. Bad for performance on modern CPUs but can be useful for code-size: [Tips for golfing in x86/x64 machine code](//codegolf.stackexchange.com/a/235553). And [Golf a Custom Fibonacci Sequence](https://codegolf.stackexchange.com/a/211331) shows disassembly both ways: a symbol restarts objdump's decoding – Peter Cordes Aug 15 '23 at 16:22