Building a basic X86 kernel part 4 - Inspecting the tools

What better way to learn the tools than through a “Hello world!” classic? Here we go:

; hello.asm
bits 32

SECTION .data
  hello   db  'Hello world!', 10
  len     equ $-hello

SECTION .text
  GLOBAL _start

_start:
  ; Print message
  mov   eax, 4		; 4 == "print"
  mov   ebx, 1		; standard output (stdout)
  mov   ecx, hello	; message to print
  mov   edx, len	; number of bytes to print
  int   80h			; make the system call

  ; Return cleanly
  mov   eax, 1		; 1 == "exit"
  mov   ebx, 0		; exit code 0 means success
  int   80h         ; make the system call

Some specifics about this sample that we did not cover yet in the previous chapter:

“hello” is initialized to “Hello world!”, 10. This is actually a simplified syntax to create an array in NASM. Each character of “Hello world!” becomes an item in the array and the newline character 10 is added at the end.

The “equ” instruction assigns a value to a symbolic name (in this case “len”). The $ means “current value of the location counter” and thus, “$-hello” means current value of the location counter minus the label hello; effectively the number of bytes between the declaration of hello and the current location. In other words, whenever we refer to len in the future, it contains the length of the string “Hello world!” plus the newline character.

“GLOBAL _start” tells NASM that _start will be accessible from the outside. This will add “_start” to the object code so the linker can find it and mark it as the entry point for your executable.

“int 80h” is a way to make a system call in the Linux kernel. It uses input in registers to determine what to do; that’s why you set EAX, EBX, ECX and EDX before you make the call. EAX contains the instruction (1 to cleanly exit the program, 4 to print some text) and the additional registers contain parameters for your call.

To compile this code into an object file, use the following command:

$ nasm -f elf -F dwarf -g -o hello.o hello.asm

The “-f elf” flag tells nasm to create a 32 bit ELF object. It is shorthand for elf32 and there is also a 64 bit counterpart named elf64. The “-F dwarf -g” bit means you want NASM to create dwarf compatible debugging symbols which we’ll use later with gdb. The “-o hello.o” flag means you want the object to be called hello.o after compilation.

With this example, it would make no difference if you specified -f elf64 and linked it into a 64 bit executable, because the instructions we use are fully compatible. The other way around (e.g. using 64 bit registers, not so much). Still, we stick with targetting purely 32 bit.

The compilation step with NASM basically validates your assembly code (syntactically) and translates it into machine code if the validation succeeds. Because we specified the ELF format, the object file starts with the ELF header.

To read this header, we can use the readelf utility and specify the -h flag:

$ readelf -h hello.o
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          64 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         17
  Section header string table index: 3
$ 

This shows we’re dealing with a so called relocatable file (as opposed to an executable file which we did not create yet) which has no entry point and no program headers. What we have here is an object file, of which typically multiple together compose an executable file through a process called linking.

Since our program consists only of this object file, we will simply link it directly into an executable using ld and analyze the ELF header again:

$ ld -m elf_i386 -o hello hello.o

$ readelf -h hello
ELF Header:
  Magic:   7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00 
  Class:                             ELF32
  Data:                              2's complement, little endian
  Version:                           1 (current)
  OS/ABI:                            UNIX - System V
  ABI Version:                       0
  Type:                              REL (Relocatable file)
  Machine:                           Intel 80386
  Version:                           0x1
  Entry point address:               0x0
  Start of program headers:          0 (bytes into file)
  Start of section headers:          64 (bytes into file)
  Flags:                             0x0
  Size of this header:               52 (bytes)
  Size of program headers:           0 (bytes)
  Number of program headers:         0
  Size of section headers:           40 (bytes)
  Number of section headers:         17
  Section header string table index: 3

Now, as you can see, we have an entry point and an executable format. This means, we can now execute our program:

$ ./hello
Hello world!
$ 

Analyzing a binary using objdump

The utility objdump allows you to analyze a binary file. It can, among other things, be used to disassemble machine code into assembly. By default, it assumes AT&T syntax which swaps source and destination operands among other things. As to not get confused, we can tell it we’re in 32 bit mode and would like to see Intel syntax by using the “-M i386,intel” flag. Additionally, because we also generate debugging symbols, we’re only interested in the .text and .data sections from our source file:

$ objdump -M i386,intel -D hello --section .text --section .data

hello:     file format elf32-i386

Disassembly of section .text:

08049000 <_start>:
 8049000:  b8 04 00 00 00         mov    eax,0x4
 8049005:  bb 01 00 00 00         mov    ebx,0x1
 804900a:  b9 00 a0 04 08         mov    ecx,0x804a000
 804900f:  ba 0d 00 00 00         mov    edx,0xd
 8049014:  cd 80                  int    0x80
 8049016:  b8 01 00 00 00         mov    eax,0x1
 804901b:  bb 00 00 00 00         mov    ebx,0x0
 8049020:  cd 80                  int    0x80

Disassembly of section .data:

0804a000 <hello>:
 804a000:  48                     dec    eax
 804a001:  65 6c                  gs ins BYTE PTR es:[edi],dx
 804a003:  6c                     ins    BYTE PTR es:[edi],dx
 804a004:  6f                     outs   dx,DWORD PTR ds:[esi]
 804a005:  20 77 6f               and    BYTE PTR [edi+0x6f],dh
 804a008:  72 6c                  jb     804a076 <_end+0x66>
 804a00a:  64 21 0a               and    DWORD PTR fs:[edx],ecx

$ 

Looking at the .text section, you can almost see a literal copy of the assembly code in our hello.asm file, except that it translated the label “hello” into a memory address and the label “len” into a constant 0xd which is hexadecimal for value 13 (which is the length of the string “Hello world!” plus the newline character).

If you look at the value of the memory address it translated for label “hello”, 0x804a000, you’ll see that it matches the location in the .data section for “<hello>”. As you’ll also see, the disassembly of the .data section shows a lot of seemingly random instructions; and they are. The disassembly feature in objdump treats the bytes of the string “Hello world!” (0x48 is hexadecimal for ‘H’, 0x65 for ‘e’, 0x6c for ‘l’, etc.) as opcodes and tries to translate them into assembly instructions.

To avoid this confusion, during analysis you’re better off splitting it up into two separate objdump calls and only disassemble the .text section, while showing the contents of .data directly:

$ objdump -s --section .data hello

Contents of section .data:
 804a000 48656c6c 6f20776f 726c6421 0a        Hello world!.

$ objdump -M i386,intel -d --section .text hello

Disassembly of section .text:

08049000 <_start>:
 8049000:  b8 04 00 00 00         mov    eax,0x4
 8049005:  bb 01 00 00 00         mov    ebx,0x1
 804900a:  b9 00 a0 04 08         mov    ecx,0x804a000
 804900f:  ba 0d 00 00 00         mov    edx,0xd
 8049014:  cd 80                  int    0x80
 8049016:  b8 01 00 00 00         mov    eax,0x1
 804901b:  bb 00 00 00 00         mov    ebx,0x0
 8049020:  cd 80                  int    0x80

Flat binaries

An ELF binary as described above is only executable in a runtime environment like Linux. When we develop something like a bootloader and a kernel, we don’t have a runtime environment yet. In fact, the ELF header and format will be entirely unusable at that point and instead, we must create something like a flat binary, which contains only the machine code for our instructions. For now, the advantage of creating a flat binary instead, is that you can more easily see what happens under the hood so you can more easily play around with instructions and see what machine code gets generated. You can create a flat binary by specifying the “-f bin” flag (and omitting the “-F dwarf -g” flags) to NASM. Then, we can use the tool hd (hexdump) to see the contents of the object file and see how things get translated.

One thing to keep in mind when using the flat binary format, is that it puts NASM in 16 bit mode by default. This means there might be a so called operand-size override prefix (0x66) before the actual opcode that may confuse you. This prefix switches from 16 to 32 bit or the other way around, depending on the default (which is 16 bit for flat binaries).

For instance, to see how the instruction “mov eax, 4” is translated to machine code, we can do the following:

$ echo 'mov eax, 4' > mov.asm
$ nasm -f bin -o mov.o mov.asm
$ hd mov.o
00000000  66 b8 04 00 00 00                                 |f.....|
00000006
$ 

You may expect ‘66’ to be the opcode here, but actually ‘b8’ is the opcode for ‘copy something to eax’ and ‘66’ is the operand-size override prefix. To prevent this, we can be explicit about our format by specifying it in the source assembly:

; mov.asm

bits 32
mov eax, 4

Now, hex dumping this flat binary will be more concise and only contain the opcode and the operand:

$ hd mov.o
00000000  b8 04 00 00 00                                    |.....|
00000005
$ 

Debugging with gdb

One of the most important things we need to be able to do is debug our code. The GNU debugger (gdb) is a very powerful debugger that we can use during the development of our kernel, but you do need to know how to use its power.

We will only outline the basic usage of gdb for now, but it’s good to know you can use the “help” command to figure out details about specific commands and possibilities.

First, let’s open gdb and point it at our sample executable:

$ gdb hello
...
Reading symbols from hello...
(gdb)

You are now in gdb. If it says “(No debugging symbols found in hello)”, you probably forgot the -g and -F flags when calling NASM (or, your .text and .data sections are in uppercase).

The “info” command is very versatile and can be used to display all kinds of information. For instance, it can show you the general outline of the loaded binary using the “target” parameter:

(gdb) info target
Symbols from "hello".
Local exec file:
  `hello', file type elf32-i386.
  Entry point: 0x8049000
  0x08049000 - 0x08049022 is .text
  0x0804a000 - 0x0804a00d is .data

Here, we see the familiar sections and it also displays our entry point, which is where execution will start if the program gets executed. If you type “help info”, you’ll get a whole list of all the information you can get from this command.

To set, list, disable, enable and remove breakpoints, the following commands are available respectively:

(gdb) break _start
Breakpoint 1 at 0x8049000: file hello.asm, line 11.

(gdb) info break
Num     Type           Disp Enb Address    What
1       breakpoint     keep y   0x08049000 hello.asm:11

(gdb) disable 1
(gdb) enable 1
(gdb) delete 1

You can also use “b” instead of typing “break”. For details on how to work with breakpoints, the “help breakpoints” is at your disposal.

To run and step through the program, the “r” command runs the program, “n” (for next) steps to the next line and “c” (for continue) runs to the next breakpoint (or program termination in this case). You can also use “s” (for step) instead of “n” to step inside a function call. To see the call stack when you’re stepping into functions, you can use the “bt” (backtrace) command.

Normally, a line of C code would translate into multiple assembly instructions. You can do line by line debugging using “n”, but to step a single instruction at a time within a group, you can also use the “ni” and “si” commands. GDB knows which assembly instructions belong to a line of C code through debugging information. For our assembly -> assembly debugging, this isn’t relevant, but worth knowing anyway.

Another interesting thing “info” can be used for, is check the values of registers. “info registers” displays all registers and their current value, but it’s also possible to specify registers you’re interested in specifically.

Let’s combine this knowledge, run our program, step through it a little and analyze the eax and ebx registers before and after they are set:

(gdb) break _start
Breakpoint 1 at 0x8049000: file hello.asm, line 11.
(gdb) run
Starting program: hello 

Breakpoint 1, _start () at hello.asm:11
11    mov    eax, 4    ; 4 == "print"
(gdb) info register eax ebx
eax            0x0                 0
ebx            0x0                 0
(gdb) n
12    mov    ebx, 1    ; standard output (stdout)
(gdb) n
13    mov    ecx, hello  ; message to print
(gdb) info register eax ebx
eax            0x4                 4
ebx            0x1                 1
(gdb) c
Continuing.
Hello world!
[Inferior 1 (process 4567) exited normally]
(gdb) 

As you can see, after the two mov instructions, the registers contain the expected values. Then, we continue execution, our “Hello world!” gets printed and the program cleanly exits.

It’s probably wise to experiment with gdb a bit and get to know it better. It’s a very powerful tool that will come in handy later.

Now, let’s start with some real work…