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>
.
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:
Opening file modes in x86 assembly Linux - how to extract O_*
macro constants from standard headers with grep
, to make something you can #include
in a .S
.
#include header with C declarations in an assembly file without errors? - in general you can extract just the defines with gcc -E -dM
on a header, e.g. to get O_RDONLY
and other constants for use with system calls.
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.