linuxnasmldelf

Why GNU `ld` has different output from NASM vs GAS `.o` files using equivalent sources?


While doing some experiments inspired by many interesting articles on tiny ELF executables, I've noticed GNU's ld generates a different executable when fed with a nasm-generated .o object file or with a (GNU)as-generated one, both using (what I presume to be¹) an equivalent assembly source.

¹: Their object files differ only on non-code sections, and are bitwise identical if I strip --strip-section-headers *.o, so I believe that's a fair assumption.

NASM (v2.16.1):

; tiny-nasm.asm
SECTION .text
GLOBAL _start
_start:
    mov      eax, 60  ; Select the _exit syscall (60 in Linux ABI)
    mov      edi, 42  ; Set the exit code argument for _exit
    syscall           ; Perform the selected syscall

GAS (v2.42):

# tiny-gas.S
.SECTION .text
.GLOBL _start
_start:
    mov      $60, %eax  # Select the _exit syscall (60 in Linux ABI)
    mov      $42, %edi  # Set the exit code argument for _exit
    syscall             # Perform the selected syscall
nasm -f elf64 tiny-nasm.asm && ld -no-pie -z noseparate-code tiny-nasm.o -o tiny-nasm.bin
as tiny-gas.S -o tiny-gas.o && ld -no-pie -z noseparate-code tiny-gas.o  -o tiny-gas.bin
strip --strip-section-headers *.bin
wc -c *.bin
diff -u <(readelf -Wa tiny-nasm.bin) <(readelf -Wa tiny-gas.bin)
132 tiny-gas.bin
140 tiny-nasm.bin
272 total

--- /dev/fd/63  2024-11-26 02:56:40.248293325 -0300
+++ /dev/fd/62  2024-11-26 02:56:40.248293325 -0300
@@ -8,7 +8,7 @@
   Type:                              EXEC (Executable file)
   Machine:                           Advanced Micro Devices X86-64
   Version:                           0x1
-  Entry point address:               0x400080
+  Entry point address:               0x400078
   Start of program headers:          64 (bytes into file)
   Start of section headers:          0 (bytes into file)
   Flags:                             0x0
@@ -25,7 +25,7 @@
 
 Program Headers:
   Type           Offset   VirtAddr           PhysAddr           FileSiz  MemSiz   Flg Align
-  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x00008c 0x00008c R E 0x1000
+  LOAD           0x000000 0x0000000000400000 0x0000000000400000 0x000084 0x000084 R E 0x1000
 
 There is no dynamic section in this file.

Both binaries work identically and as expected, and their only difference is the entry point address offset chosen by ld, which accounts for the file size, 8 bytes smaller for as.

As the arguments were identical, I assume the explanation is in the (stripped) sections, which I've omitted here for brevity but can include if needed. But what in those sections could trigger the different entry point address chosen by ld?

If relevant: Ubuntu 24.04, Linux desktop 6.8.0-48 x86_64, binutils 2.42, AMD Ryzen 5 5700G


Solution

  • As documented in the NASM manual the default attributes for the ELF section .text are section .text progbits alloc exec nowrite align=16.

    Apparently gas has a lower alignment, below-or-equal 8, so that's how you observed a difference. The solution is to specify an align=1 attribute in the section directive, like so:

    section .text align=1