This was originally posted on Google+, which has now been shut down. It was helpfully converted to Markdown by Robert Jacobson after which I adjusted it for reposting here. I have dated it at its original posting date, but it was posted here on 2019-09-07.
Aside: For your convenience, please use a
Makefile. To assemble and link, simply execute
# Please be aware that the indentation must be a tab character (no spaces) all: hello true %: %.o ld -macosx_version_min 10.6 -o $@ -e main $< %.o: %.asm nasm -f macho64 -o $@ $<
Step 3: Calling write
We are now ready for "Hello world"! We know how to do a syscall, so let's see (in
/usr/include/sys/syscall.h) if we can use one for printing.
"SYS_write" (4) looks promising, and in fact it is the POSIX write that has a documented C API we can read with "man 2 write":
ssize_t write(int fildes, const void *buf, size_t nbyte);
Oh, wow. There's lots of arguments and datatypes and stuff. Again, the ABI tells us to put arguments in sequence in the registers
r9. And if we need more arguments, we will have to look at the ABI again.
filedes: We want to write to standard out, and its fileno is defined in POSIX as 1. We can verify this information against
STDOUT_FILENO is defined as 1.
buf: We need a string to write. Let's postpone that slightly!
nbyte: This is the count of bytes to write. Let's write 0 of them for now.
This should translate to the following assembly:
mov rax, 0x02000004 ; Again, 0x02000000 added for a "Unix" type call mov rdi, 1 ; filedes = 1 mov rsi, 0xdeadbabe ; Filler value, we postponed the problem of buf mov rdx, 0 ; Write zero bytes from this buffer syscall
Combining with the skeleton "true.asm" you should be able to come up with an "hello.asm" that looks something like this:
global main main: mov rax, 0x02000004 ; SYS_write mov rdi, 1 ; filedes = STDOUT_FILENO = 1 mov rsi, 0xdeadbabe ; Filler value, we postponed the problem of buf mov rdx, 0 ; Write zero bytes from this buffer syscall mov rax, 0x02000001 ; SYS_exit mov rdi, 0 ; Process exit value syscall
When assembled and linked, this should reliably execute without writing anything or failing. Even though we give a bogus pointer value in, it never gets dereferenced since the nbyte argument is 0.
Now we are calling
write! Let's give it something to say.
Step 4: Data section
We need a string constant to write to stdout, and we are going to put one in static storage. We do this by putting in stuff for the assembler and linker that isn't assembly code. First, we partition the file into two sections, one for data and one for code. The one for data is called
.data and the one for code is called
Put a line with
section .data at the top of your file, put in a couple of blank lines and then
section .text as a header for your code. Everything that's under the
section .data header, but above the
section .text header will now be said to be in the data section, while everything underneath the
section .text header will be said to be in the text section. It should now look like this:
.section .data .section .text global main main: mov rax, 0x02000004 ; SYS_write mov rdi, 1 ; filedes = STDOUT_FILENO = 1 mov rsi, 0xdeadbabe ; Filler value, we postponed the problem of buf mov rdx, 0 ; Write zero bytes from this buffer syscall mov rax, 0x02000001 ; SYS_exit mov rdi, 0 ; Process exit value syscall
In the data section, put a definition for a string. It is going to be data that we specify bytewise, so we are going to use the keyword db:
hello_world db "Hello world!", 0x0a
0x0a is the control character for a newline. In C we would have used
"\n", which can be mapped to other values depending on the platform. C is much more cross platform compatible than assembly!
Now, we can update the call to
write with proper values:
mov rsi, hello_world ; The assembler and/or linker will make sure rsi gets the address of the string above mov rdx, 13 ; The string, including the newline, is 13 bytes long
The file should now look like this:
section .data hello_world db "Hello World!", 0x0a section .text global main main: mov rax, 0x02000004 ; SYS_write mov rdi, 1 ; filedes = STDOUT_FILENO = 1 mov rsi, hello_world ; The address of hello_world string mov rdx, 13 ; The size to write syscall mov rax, 0x02000001 ; SYS_exit mov rdi, 0 ; Exit status syscall
Congratulations! You have implemented "Hello world" in assembly! :)
Step 5: Disassembly
A big point of knowing assembly is to know exactly what is happening under the hood. However, the
mov rsi, hello_world instruction above gets lots of interpretation. Let's look closer with otool.
First, look at the data section:
otool -d ./hello
The output I get is:
./hello: (__DATA,__data) section 0000000000002000 48 65 6c 6c 6f 20 57 6f 72 6c 64 21 0a
0000000000002000 is the address where the data section starts, and the bytes from
0a is the string we put in, ASCII coded and presented in hex. Nice. This is pretty much exactly what we put in. Notice that there is no label, no information on structure inside the data section.
Next, look at the text/code section:
otool -t ./hello ./hello: (__TEXT,__text) section 0000000000001fd9 b8 04 00 00 02 bf 01 00 00 00 48 be 00 20 00 00 0000000000001fe9 00 00 00 00 ba 0d 00 00 00 0f 05 b8 01 00 00 02 0000000000001ff9 bf 00 00 00 00 0f 05
Oh. That's not very readable. We can get otool to disassemble it for us, but beware, for it uses the ugly AT&T syntax:
otool -tv ./hello ./hello: (__TEXT,__text) section main: 0000000000001fd9 movl $0x02000004,%eax 0000000000001fde movl $0x00000001,%edi 0000000000001fe3 movq $0x0000000000002000,%rsi 0000000000001fed movl $0x0000000d,%edx 0000000000001ff2 syscall 0000000000001ff4 movl $0x02000001,%eax 0000000000001ff9 movl $0x00000000,%edi 0000000000001ffe syscall
The order of the operands to mov are backwards! Sigh. The big number on the left is just the address of each instruction.
This looks pretty much like what we put it. The most interesting difference is the line where we set rsi. In our source code it is
mov rsi, hello_world, but the result, translated to Intel syntax, is
mov rsi, 0x0000000000002000. This is the address in the data section where our string starts, so this is pretty good news :) This gives an insight into how symbols are dereferenced during assembling and linking.
The second thing we notice is that some of our "r"-s have been turned into "e"-s. This is an optimization nasm put in for us. The registers, as we know, have not always been 64 bits wide, so there is some legacy here. Let's consider
rax as an example:
rax, you speak of the entire 64 bit register. eax is the low 32 bits. The low 16 bits are called ax and the low 8 bits
al. Additionally, bits 8-15 can be accessed directly as
When defining the x86-64 instruction set, AMD realized that requiring you to put 64 bits of data into the text section every time you wanted to set a register would quickly fill up code with lots of worthless 0 bytes, in turn filling up expensive cache lines in the processor with the same. So as a special rule, when setting a 32 bit register, the high 32 bits always get nulled out. So
mov eax, 0x12345678 has the same effect as
mov rax,0x0000000012345678, but the
eax one is four bytes smaller than the rax one.
Now we can understand the disassembly from otool, and how it relates to the original source code. Hooray!
Achievement awarded: Followed a programming tutorial where "Hello world" was the second lesson.
Next lesson: Asmtut 3: Redundancies