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 I instead 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>
.
CodePudding user response:
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 withgrep
, 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 getO_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