Search code examples
assemblyx86c-preprocessor32bit-64bitgnu-assembler

gnu GAS use stdio.h in asm and compile 32 bit on 64 bit host


I want to compile the following asm.S file

#include <stdio.h>
.global _start
.text
message_text:
  call program
str: .string "hello world"
  .equ len, (. - str)

_start:
  jmp message_text
program:
  call puts@PLT

I have tried gcc -m32 asm.S as well as gcc -m32 asm.S -fPIC and a litany of other things but I keep running into errors such as

/usr/include/stdio.h:847: Error: no such instruction: extern void ...

What am I doing wrong?

I get that the preprocessor is just inserting the header into the asm file and so the assembler is getting erroring out b/c C function prototypes are not assembly instructions. However I have been able to use #include <asm/unistd.h> and #include <syscall.h> with success. So why won't stdio.h work?

As an aside, I tried commenting out the header and just keeping the line call puts@PLT and was able to compile via

as --32 -o output.o asm.S
ld -m elf_i386 -o output output.o -lc

However now, when I try ./output I get

no such file or dirctory: ./output

despite output showing up in ls and if I run file output I get

output: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /usr/bin/libc.so.1, not stripped

If instead I do

as --32 -o output.o asm.S
ld -m elf_i386 -dynamic-linker /lib/ld-linux.so.2 -o output -lc output.o

I can run the resulting binary. But again, this is if I remove #include <stdio.h>.


Solution

  • stdio.h is a C header with C syntax, not just C preprocessor stuff. In asm you don't need C prototypes or variable declarations; in GAS any undeclared symbol is assumed to be extern so you can just call puts@plt without any other directives. (Unlike NASM where you would need extern puts, which is different syntax from C so the C header wouldn't help anyway.)

    A few C headers like <asm/unistd.h> contain only #if and #define preprocessor directives, and can be included in .S files as well. Most headers can't, although the Linux kernel's asm/*.h typically can.

    Standard headers could have been written to use #ifndef __ASSEMBLY__ around C specific parts, which would have been useful for headers like <fcntl.h> which have constants like O_RWDR and O_TRUNC as well as C prototypes, but they weren't.

    The stdio API predates the C preprocessor, which is why it unfortunately uses strings like fopen(name, "r+") instead of ORing constants. So this isn't necessary or useful on stdio.h.

    See also:


    BTW, your jmp / call trick to push a string address looks like shellcode. But you can't call puts@plt in shellcode because the relative offset from a stack buffer to that PLT entry will be randomized.

    Or perhaps it's just a hacky way to make 32-bit PIC code, in which case it's compact-ish but inefficient: defeats return-address prediction. (Mismatched call/ret). Look at how gcc or clang -O2 -m32 -fPIC compiles C code, getting an address for the GOT into a register with call; note that call next_instruction (relative offset of 0) is special cased to not count for return-address prediction on most CPUs. See Reading program counter directly

    This is clang -O2 -m32 -fPIE output from the Godbolt compiler explorer with necessary directives kept, unnecessary ones manually removed. (Godbolt's directives filter will normally filter everything, assuming that you know which ones are needed.)

    This is the standard way to get a static address into a register in position-independent 32-bit code. (Nevermind that it's a main so it returns instead of calling exit. Or _exit or a raw system call, but don't do that in programs that use stdio functions; that wouldn't flush stdout if there was buffered output left, e.g. because of writing to a pipe.)

            .text
            .globl  main                            # -- Begin function main
    main:
            pushl   %ebx
            subl    $8, %esp
            calll   .L0$pb
    .L0$pb:
            popl    %ebx
    .Ltmp0:
            addl    $_GLOBAL_OFFSET_TABLE_+(.Ltmp0-.L0$pb), %ebx   # pointer to the GOT
            leal    .L.str@GOTOFF(%ebx), %eax           # .L.str address relative to GOT
            movl    %eax, (%esp)
            calll   puts@PLT
    .Ltmp1:
            addl    $8, %esp
            popl    %ebx
            retl                 # note that _start can't ret, it has to call exit.
    
    #   .section        .rodata.str1.1,"aMS",@progbits,1  # clang used this
        .section        .rodata                      # a human would normally do this
    .L.str:
            .asciz  "hello"
    

    This two-step calculation getting a GOT base is not necessary since we don't use the global offset table for anything else; it's just the compiler's choice of reference anchor for position-independent code in 32-bit mode (where RIP-relative addressing isn't available). We could add $.L.str - .L0$pb, %ebx after the pop. And we could have used %eax so we didn't have to save/restore EBX.

    It's a lot simpler to use position-dependent code in 32-bit so you can just push $.L.str, or to use 64-bit code where you'd lea .L.str(%rip), %rdi ; call puts@plt.


    interpreter /usr/bin/libc.so.1

    There's your problem. That path probably doesn't exist, and certainly isn't the runtime dynamic linker /lib/ld-linux.so.2 (aka ELF interpreter). Use gcc -m32 -nostartfiles if you want to write your own _start, but have it make a dynamically linked executable with libc and the right ELF interpreter. (Use gcc -v to see what args it passes to the assembler and linker).

    IDK why your ld would have a weird default like that; usually the default interpreter path is right for 32-bit mode but wrong for 64-bit executables.

    See also Assembling 32-bit binaries on a 64-bit system (GNU toolchain) re: build commands and _start vs. main.

    Note that calling libc functions from your own _start only works in a dynamically linked executable because libc uses dynamic linker hooks to get its init functions called before your _start runs. If you'd used -static, code in puts would crash.