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 :
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.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With