Asmtut 2: Hello world!

by
2012-10-07T11:00:00Z

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 make.

# 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 rdi, rsi, rdx, rcx, r8 and 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 /usr/include/unistd.h, where 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, 0x1337c0d3 ; 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, 0x1337c0d3     ; 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 .text.

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, 0x1337c0d3     ; 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 48 through 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:

Using 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 ah.

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

, 2012