Assembling a Minimal x86-64 Binary
We will compile -and link- a few lines of assembly, using GAS (GNU Assembler) in Linux.
[ Check out all posts in “low-level” series here. ]
I was reading Programming from the Ground Up by Jonathan Bartlett some time ago. ( It is good. )
It is based on x86, Linux, with GNU toolchain.
The first example is a program that just exits with a specific status code. The file prog_g32.s
looks like this:
.section .data
.section .text
.global _start
_start:
movl $1, %eax
movl $0, %ebx
int $0x80
Here is an overview of the code: 1
- We defined the label
_start
under the.text
section. - We made it visible to the linker by marking it
.global
. - We authored the actual CPU instructions.
The label _start
is the default executable entry point name ld
will look for during link. 2
“Exiting” involves making a system call. So the instructions (following _start
) are about setting up the inputs to the call, and executing it.
Let’s look at the instructions line-by-line:
%eax
: Holds the system call number for x86.
movl $1, %eax # System call number for sys_exit: 1
%ebx
: Holds the return status ( first syscall param for x86 calling conventions ).
movl $0, %ebx # Exit status number: 0
Interrupt to notify kernel to make the system call.
int $0x80 # Deprecated ( and probably linux specific ).
Now, this seems to still work when we build an x86-64 executable.
However, there is a better way in x86-64 world for making system calls: syscall
instruction.
(Other fast entry instructions exist for x86 and x86-64, but I will omit them.)
The version I modified to use syscall (named prog_g64.s
):
.section .data
.section .text
.global _start
_start:
mov $60, %rax
mov $0, %rdi
syscall
As you can see, every line is different. Here is the explanation:
%rax
: Holds the system call number for syscall in x86-64.
mov $60, %rax # System call number for sys_exit in x86-64 is different: 60
%rdi
: Holds the return status ( first syscall param for x86-64 calling conventions ).
mov $0, %rdi # Exit status number: 0
Portable x86-64 insn for fast system call:
syscall
(And nowadays there is an even more optimized way to make system calls. I might talk about it in a later post.)
Here is the makefile that I use to build both versions:
# Shell replacement to print the rule being run.
OLD_SHELL := $(SHELL)
SHELL := $(warning Building $@$(if $<, (from $<))$(if $?, ($? newer)))$(OLD_SHELL)
DEPS =
OBJ = prog
LD = ld
.PRECIOUS: %.s _%.o
_%_g32.o : %_g32.s $(DEPS) ; @echo "RULE> $@ : $^" ; as -o $@ $< --32
_%_g64.o : %_g64.s $(DEPS) ; @echo "RULE> $@ : $^" ; as -o $@ $< --64
prog_g32 : $(patsubst %,_%,$(OBJ)_g32.o) ; @echo "RULE> $@ : $^" ; $(LD) $^ -o $@ -m elf_i386
prog_g64 : $(patsubst %,_%,$(OBJ)_g64.o) ; @echo "RULE> $@ : $^" ; $(LD) $^ -o $@ -m elf_x86_64 -pie -static -no-dynamic-linker
Below will make the x86 (i386) binary:
make prog_g32
And to make the x86-64 binary:
make prog_g64
After running the program, you can echo $?
to print the status number returned from the previous command. Both should return “0”.
-
I am not very experienced in assembly. If you find errors, please report in the blog’s Issues page. ↩
-
The name of the entry point
ld
looks for can be modified. ↩