Skip to content

Latest commit

 

History

History
284 lines (224 loc) · 11.3 KB

17_loading_tasks_from_disk_p2.md

File metadata and controls

284 lines (224 loc) · 11.3 KB

Loading Tasks from Disk — Part 2

Our goal now is to compile and link tasks separate from the kernel. We'll generate binary from each task and append them to the overall disk image right after the kernel.

In order to do so, we'll need to prepare a separate linker script for task linking. Let's remove that part of the original linker script and move it to a new file.

/* kernel.ld */

SECTIONS
{
    .kernel 0x7e00 :
    {
        kernel.o(.text .data .rodata)
        *(.text .data .rodata)
        . = ALIGN(512);
    }
}
/* task.ld */

SECTIONS
{
    .task :
    {
        *(.text .data .rodata)
        . = ALIGN(512);
    }
}

Let's make corresponding changes to the Makefile to separate kernel linking from task linking.

# Makefile

.DEFAULT_GOAL := all

NASM := nasm
GCC := i386-elf-gcc
LD := i386-elf-ld
QEMU := qemu-system-i386

CFLAGS := -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP
LDFLAGS := --oformat=binary

%.o: %.c
	$(GCC) $(CFLAGS) -c $< -o $@

##
# boot sector
#
bootsect.bin: bootsect.asm
	$(NASM) $< -o $@

##
# kernel
#
KERNEL_LDFLAGS := $(LDFLAGS) --entry=kmain

KERNEL_SRCS = kernel.c console.c cpu.c task.c port.c ata.c
KERNEL_OBJS = $(KERNEL_SRCS:.c=.o)
KERNEL_DEPS = $(KERNEL_SRCS:.c=.d)

kernel.bin: $(KERNEL_OBJS) kernel.ld
	$(LD) $(KERNEL_LDFLAGS) $(KERNEL_OBJS) -T kernel.ld -o $@

##
# tasks
#
task_a.bin: task_a.o task.ld
	$(LD) $(LDFLAGS) $< -T task.ld -o $@

task_b.bin: task_b.o task.ld
	$(LD) $(LDFLAGS) $< -T task.ld -o $@

##
# OS image

os.img: bootsect.bin kernel.bin task_a.bin task_b.bin
	cat $^ > os.img

##
# other targets
#
.PHONY: all run clean

all: os.img

run: os.img
	$(QEMU) -nic none -drive file=$<,format=raw -monitor stdio

clean:
	$(RM) $(KERNEL_OBJS) $(KERNEL_DEPS) task_*.o task_*.d *.bin os.img

-include $(deps)

Let's make sure everything builds properly.

$ make
nasm bootsect.asm -o bootsect.bin
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c kernel.c -o kernel.o
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c console.c -o console.o
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c cpu.c -o cpu.o
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c task.c -o task.o
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c port.c -o port.o
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c ata.c -o ata.o
i386-elf-ld --oformat=binary --entry=kmain kernel.o console.o cpu.o task.o port.o ata.o -T kernel.ld -o kernel.bin
i386-elf-ld: task.o:/Users/khaledhammouda/src/osdev/bitflow/task.c:13: undefined reference to `__tasks_start'
make: *** [kernel.bin] Error 1

Since we removed the tasks from the kernel linker script, along with __tasks_start symbol, the linker is complaining that the symbol is undefined because we still reference it from the task.c module that we created to load a task from an section embedded within the kernel. We're going to rewrite this module soon, so let's remove the code there and keep it empty for now. After removing the code in task.c let's build again.

$ make
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c task.c -o task.o
i386-elf-ld --oformat=binary --entry=kmain kernel.o console.o cpu.o task.o port.o ata.o -T kernel.ld -o kernel.bin
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c task_a.c -o task_a.o
i386-elf-ld --oformat=binary task_a.o -T task.ld -o task_a.bin
i386-elf-ld: task_a.o: in function `task_a':
/Users/khaledhammouda/src/osdev/bitflow/task_a.c:5: undefined reference to `kput_str'
make: *** [task_a.bin] Error 1

This time the error is in linking task_a.o, which still is trying to call the kernel function through the pointer variable kput_str, but that symbol cannot be resolved since we've now separated the task from the kernel. So how do get the address of that function? Unfortunately the linker won't be able to help us here, we'll need to find another way.

A simple (but unmaintainable) approach is to find the function pointer address manually (by inspecting the kernel link map) and hard code it in the task. Let's add the --print-map flag temporarily to the KERNEL_LDFLAGS variable in the makefile, and build the kernel.

$ make kernel.bin | grep put_str$
                0x0000000000007f69                put_str
                0x000000000000801c                kput_str

The put_str symbol is the actual function address, while kput_str is a pointer variable that points to the function. Let's hard code the actual function address by assigning it to a function pointer variable within the task itself.

// task_a.c

...

void (*kput_str)(char* str, char attr, int row, int col) = (void *)0x7f69;

void task_a() {
    kput_str("Task A", WHITE_ON_BLUE, 1, 0);
}

Let's compile and link the task and take a look at the assembled binary.

$ make task_a.bin
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c task_a.c -o task_a.o
i386-elf-ld --oformat=binary task_a.o -T task.ld -o task_a.bin

$ ndisasm -u task_a.bin | head -20
00000000  55                push ebp
00000001  89E5              mov ebp,esp
00000003  83EC08            sub esp,byte +0x8
00000006  A120000000        mov eax,[0x20]
0000000B  6A00              push byte +0x0
0000000D  6A01              push byte +0x1
0000000F  6A1F              push byte +0x1f
00000011  6824000000        push dword 0x24
00000016  FFD0              call eax
00000018  83C410            add esp,byte +0x10
0000001B  90                nop
0000001C  C9                leave
0000001D  C3                ret
0000001E  6690              xchg ax,ax
00000020  697F0000546173    imul edi,[edi+0x0],dword 0x73615400
00000027  6B2041            imul esp,[eax],byte +0x41
0000002A  006690            add [esi-0x70],ah
0000002D  6690              xchg ax,ax
0000002F  6690              xchg ax,ax
00000031  6690              xchg ax,ax

The call to the kernel function is being made at address 16: call eax. This is an indirect call through the eax register, which was loaded with the instruction at address 6: mov eax,[0x20]. So the function pointer variable is stored at address 0x20 (which is part of the .rodata section). At address 0x20 the disassembler is treating data as code (it doesn't know it's data), so we should focus our attention on the first 4 bytes at that address, which is the function pointer value: 697F0000 which we need to parse as little endian as 0x00007F69. This is indeed our hard coded kernel function address.

There's a problem though. We're assuming that the code above starts at address 0, which is incorrect. The kernel loads the task binary at address 0xf000 before executing it. So let's tell ndisasm that the origin of the binary is 0xf000 instead of 0.

❯ ndisasm -u -o 0xf000 task_a.bin | head -20
0000F000  55                push ebp
0000F001  89E5              mov ebp,esp
0000F003  83EC08            sub esp,byte +0x8
0000F006  A120000000        mov eax,[0x20]
0000F00B  6A00              push byte +0x0
0000F00D  6A01              push byte +0x1
0000F00F  6A1F              push byte +0x1f
0000F011  6824000000        push dword 0x24
0000F016  FFD0              call eax
0000F018  83C410            add esp,byte +0x10
0000F01B  90                nop
0000F01C  C9                leave
0000F01D  C3                ret
0000F01E  6690              xchg ax,ax
0000F020  697F0000546173    imul edi,[edi+0x0],dword 0x73615400
0000F027  6B2041            imul esp,[eax],byte +0x41
0000F02A  006690            add [esi-0x70],ah
0000F02D  6690              xchg ax,ax
0000F02F  6690              xchg ax,ax
0000F031  6690              xchg ax,ax

If you look at the mov instruction (now at address 0xF006) that loads the eax register with the contents of the function pointer variable, you'll notice that it still thinks that the pointer variable is stored at 0x20. That's not good, because it is clearly stored at 0xF020.

You'll notice the same problem with the instruction at address 0xF011: push dword 0x24. This is the instruction that pushes the address of the string "Task A" on the stack to pass it to the kernel function. The string will be actually stored at 0xF024 when the task is loaded into memory (you can see the ASCII code of the characters - and the terminating zero - starting at that address: 54 61 73 6B 20 41 00).

If we run this code it won't work. The problem is that the linker by default assumes the code is going to be linked with a starting address of 0.

We can do something similar to what we did for the kernel by telling the linker that the kernel starting address is 0x7e00. Let's modify the task.ld script to tell the linker that the task will be loaded at address 0xf000 in memory.

/* task.ld */

SECTIONS
{
    .task 0xf000 :
    {
        *(.text .data .rodata)
        . = ALIGN(512);
    }
}

Let's build the task and inspect its binary again.

$ make task_a.bin
i386-elf-gcc -g -fno-asynchronous-unwind-tables -ffreestanding -masm=intel -MMD -MP -c task_a.c -o task_a.o
i386-elf-ld --oformat=binary task_a.o -T task.ld -o task_a.bin

$ ndisasm -u -o 0xf000 task_a.bin | head -20
0000F000  55                push ebp
0000F001  89E5              mov ebp,esp
0000F003  83EC08            sub esp,byte +0x8
0000F006  A120F00000        mov eax,[0xf020]
0000F00B  6A00              push byte +0x0
0000F00D  6A01              push byte +0x1
0000F00F  6A1F              push byte +0x1f
0000F011  6824F00000        push dword 0xf024
0000F016  FFD0              call eax
0000F018  83C410            add esp,byte +0x10
0000F01B  90                nop
0000F01C  C9                leave
0000F01D  C3                ret
0000F01E  6690              xchg ax,ax
0000F020  697F0000546173    imul edi,[edi+0x0],dword 0x73615400
0000F027  6B2041            imul esp,[eax],byte +0x41
0000F02A  006690            add [esi-0x70],ah
0000F02D  6690              xchg ax,ax
0000F02F  6690              xchg ax,ax
0000F031  6690              xchg ax,ax

The mov eax,[0xf020] instruction now refers to the correct memory address of the function pointer at 0xf020. The push dword 0xf024 instruction also refers to the correct memory address of the string.

Let's build everything and try it out.

Booting kernel...
Task A
Task B

It worked! It's definitely not a maintainable setup, but at least proved to ourselves that we can compile/link a task separately from the kernel, and have the kernel load and execute it, and the task is able to call a kernel function at a specific address.

Recap

  • We separated the tasks from the kernel so that we can compile and link them separately.
  • We analyzed the task binary and realized that addresses in the binary are relative to 0, not to where the task will be loaded in memory.
  • We were able to tell the linker about the load address of the task and got the addresses corrected.
  • We were able to load and execute the task, and have it still call the kernel function by hard coding its address in the task.

Next

Having hard coded function addresses won't get us far, since any change the kernel may cause the function address to change. We will need to improve how a task finds and calls a kernel function.