Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Unable to generate smallest binary from Rust code

Tags:

gcc

rust

objdump

My goal is to compile Rust programs to the smallest possible binary and extract the machine code. I've made a very simple program to test.

.cargo/config

[target.x86_64-pc-windows-gnu]
rustflags = ["-C", "link-args=-e _start -static -nostartfiles"]

Cargo.toml

[package]
name = "r"
version = "0.1.0"
edition = "2021"

[profile.release]
panic = "abort"
opt-level = "z"
lto = true
codegen-units = 1

main.rs

#![no_std]
#![no_main]

#[panic_handler]
fn panic(_: &core::panic::PanicInfo) -> ! {
    loop {}
}

#[no_mangle]
unsafe fn _start() -> isize {
    42
}

I compile cargo build --target x86_64-pc-windows-gnu --release, extract the .text section objcopy -j .text -O binary target/x86_64-pc-windows-gnu/release/r.exe r.bin, but when I display the machine code, I get more than I expected :

% objdump -D -b binary -mi386 -Mx86-64 -Mintel -z r.bin 

r.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   b8 2a 00 00 00          mov    eax,0x2a
   5:   c3                      ret
   6:   66 90                   xchg   ax,ax
   8:   ff                      (bad)
   9:   ff                      (bad)
   a:   ff                      (bad)
   b:   ff                      (bad)
   c:   ff                      (bad)
   d:   ff                      (bad)
   e:   ff                      (bad)
   f:   ff 00                   inc    DWORD PTR [rax]
  11:   00 00                   add    BYTE PTR [rax],al
  13:   00 00                   add    BYTE PTR [rax],al
  15:   00 00                   add    BYTE PTR [rax],al
  17:   00 ff                   add    bh,bh
  19:   ff                      (bad)
  1a:   ff                      (bad)
  1b:   ff                      (bad)
  1c:   ff                      (bad)
  1d:   ff                      (bad)
  1e:   ff                      (bad)
  1f:   ff 00                   inc    DWORD PTR [rax]
  21:   00 00                   add    BYTE PTR [rax],al
  23:   00 00                   add    BYTE PTR [rax],al
  25:   00 00                   add    BYTE PTR [rax],al
  27:   00                      .byte 0x0

I expected it :

% objdump -D -b binary -mi386 -Mx86-64 -Mintel -z r.bin 

r.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   b8 2a 00 00 00          mov    eax,0x2a
   5:   c3                      ret

Two questions :

  • What is the purpose of the machine code from address 0x6 to the end of the file ?
  • How do I get rid of it ?
like image 313
Xobtah Avatar asked Jul 03 '26 18:07

Xobtah


1 Answers

These additional instructions you are referring to aren't actual instructions; you can see some clues on what these could be by looking at the symbols pointing to them, for instance by executing objdump --all -S target/x86_64-pc-windows-gnu/release/rust-test.exe:

0000000140001000 <_start>:
   140001000:   b8 2a 00 00 00          mov    $0x2a,%eax
   140001005:   c3                      ret
   140001006:   66 90                   xchg   %ax,%ax

0000000140001008 <__CTOR_LIST__>:
   140001008:   ff                      (bad)
   140001009:   ff                      (bad)
   14000100a:   ff                      (bad)
   14000100b:   ff                      (bad)
   14000100c:   ff                      (bad)
   14000100d:   ff                      (bad)
   14000100e:   ff                      (bad)
   14000100f:   ff 00                   incl   (%rax)
   140001011:   00 00                   add    %al,(%rax)
   140001013:   00 00                   add    %al,(%rax)
   140001015:   00 00                   add    %al,(%rax)
    ...

0000000140001018 <__DTOR_LIST__>:
   140001018:   ff                      (bad)
   140001019:   ff                      (bad)
   14000101a:   ff                      (bad)
   14000101b:   ff                      (bad)
   14000101c:   ff                      (bad)
   14000101d:   ff                      (bad)
   14000101e:   ff                      (bad)
   14000101f:   ff 00                   incl   (%rax)
   140001021:   00 00                   add    %al,(%rax)
   140001023:   00 00                   add    %al,(%rax)
   140001025:   00 00                   add    %al,(%rax)
    ...

These additional symbols, __CTOR_LIST__ and __DTOR_LIST__ are related to the feature called global constructors. This feature (used primarily in C++) allows you to annotate functions with __attribute__((constructor)) and __attribute__((destructor)) and these attributes cause the functions to be called at the start and end of a runtime of your program, respectively.

Now, this feature just happens to be implemented by appending the addresses of these functions at the end of the .text section in MinGW (they are implemented differently not only on GCC targeting Linux, but also on LLVM LLD targeting MinGW) — see this answer for more details on the implementation. The mingw-w64 runtime just calls the constructor in the __main function:

for (i = nptrs; i >= 1; i--)
  {
    __CTOR_LIST__[i] ();
  }

In order to get rid of this additional data, you can use a custom linker script for ld. You can see the default script which is called i386pep.x and put in your MinGW installation path (usually /usr/x86_64-w64-mingw32/lib/ldscripts/i386pep.x or similar)[1]. You can copy this file to some other place, and when you open it, you should see the following code there:

.text  __image_base__ + ( __section_alignment__ < 0x1000 ? . : __section_alignment__ ) :
{
  KEEP (*(SORT_NONE(.init)))
  *(.text)
  *(SORT(.text$*))
   *(.text.*)
   *(.gnu.linkonce.t.*)
  *(.glue_7t)
  *(.glue_7)
  . = ALIGN(8);
     /* Note: we always define __CTOR_LIST__ and ___CTOR_LIST__ here,
        we do not PROVIDE them.  This is because the ctors.o startup
  code in libgcc defines them as common symbols, with the
        expectation that they will be overridden by the definitions
  here.  If we PROVIDE the symbols then they will not be
  overridden and global constructors will not be run.
  See PR 22762 for more details.

  This does mean that it is not possible for a user to define
  their own __CTOR_LIST__ and __DTOR_LIST__ symbols; if they do,
  the content from those variables are included but the symbols
  defined here silently take precedence.  If they truly need to
  be redefined, a custom linker script will have to be used.
  (The custom script can just be a copy of this script with the
  PROVIDE() qualifiers added).
  In particular this means that ld -Ur does not work, because
  the proper __CTOR_LIST__ set by ld -Ur is overridden by a
  bogus __CTOR_LIST__ set by the final link.  See PR 46.  */
     ___CTOR_LIST__ = .;
     __CTOR_LIST__ = .;
     LONG (-1); LONG (-1);
     KEEP (*(.ctors));
     KEEP (*(.ctor));
     KEEP (*(SORT_BY_NAME(.ctors.*)));
     LONG (0); LONG (0);
     /* See comment about __CTOR_LIST__ above.  The same reasoning
      applies here too.  */
     ___DTOR_LIST__ = .;
     __DTOR_LIST__ = .;
     LONG (-1); LONG (-1);
     KEEP (*(.dtors));
     KEEP (*(.dtor));
     KEEP (*(SORT_BY_NAME(.dtors.*)));
     LONG (0); LONG (0);
  KEEP (*(SORT_NONE(.fini)))
  /* ??? Why is .gcc_exc here?  */
   *(.gcc_exc)
  PROVIDE (etext = .);
   KEEP (*(.gcc_except_table))
}

Since you don't really use the mingw64 runtime's __main function (as you define your own _start), you can safely remove all of stuff belonging to __CTOR_LIST__ and leave just the following:

.text  __image_base__ + ( __section_alignment__ < 0x1000 ? . : __section_alignment__ ) :
{
  KEEP (*(SORT_NONE(.init)))
  *(.text)
  *(SORT(.text$*))
   *(.text.*)
   *(.gnu.linkonce.t.*)
  *(.glue_7t)
  *(.glue_7)
  . = ALIGN(8);
  KEEP (*(SORT_NONE(.fini)))
  /* ??? Why is .gcc_exc here?  */
   *(.gcc_exc)
  PROVIDE (etext = .);
   KEEP (*(.gcc_except_table))
}

After that, you can modify your .cargo/config.toml file to include the path to your custom linker script:

rustflags = ["-C", "link-args=-e _start -static -nostartfiles -Wl,-T,<path-to-your-script-dir>/i386pep.x"]

And that's it! After compiling your project, you can see this strange data is gone now:

$ objdump -D -b binary -mi386 -Mx86-64 -Mintel -z r.bin

r.bin:     file format binary


Disassembly of section .data:

00000000 <.data>:
   0:   b8 2a 00 00 00          mov    eax,0x2a
   5:   c3                      ret

[1]I can't really link to this file on any "official" repository since it's generated from the template as part of the MinGW/binutils build system, but you can see the traces here.

like image 61
m4tx Avatar answered Jul 06 '26 10:07

m4tx