OSdevSetup

Develop simple OS on linux

after days of struggling, I finally figure out a way to do the things told by the book 《30日でできる! OS自作入門》 without author’s highly customized toolchain

Referrence

Thanks a lot to those gracious people

How to

1. You need a floppy disk to store you code

1
$ qemu-img create -f raw floppy.img 4M

2. Write Initial Program Loader(ipl) code and make it into a bootsector

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
; ipl.s
[org 0x7c00]

; ---------------------------------- preparing stage -----------------------

cli ; enable Qemu hlt, see more at stackoverflow why qemu hlt doesn't work

mov [BOOT_DISK], dl ; when loading ipl into 0x7c00, bios will store disk number into dl

setstack:
mov bp , 0x7c00
mov sp , bp ; stack region 0x7c00~0x0000
; now we can use call and ret instruction

; ---------------------------------- preparing stage -------------------------------


mov di,prompt
call printstring
call readdisk
jmp PROGRAM_SPACE


prompt:
db 'hello world!' , 0


%include "helpfuncs.s"
%include "readdisk.s"
; the %include simply copy the content in

times 510 - ($ - $$) db 0
db 0x55
db 0xaa
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
; helpfuncs.s


; string address : di
; return value : None
printstring:
mov al, [di] ; default ds is 0x0000 , so it's ok to do this
cmp al,0
jz end
mov ah,0x0e
int 0x10
inc di
jmp printstring
end:
ret
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
; readdisk.s

BOOT_DISK:
db 0


PROGRAM_SPACE equ 0x7e00 ; rest of our floppy will be read into 0x7e00, right after our ipl
SECTOR_NUM equ 4 ; how many sectors we need to read into mem

; 0xfffff *-------------------*
; | unused |
; 0x8000 *-------------------*
; | program_space |
; 0x7e00 *-------------------* <- cs:ip , go upside
; | ipl |
; 0x7c00 *-------------------* <- sp,bp , go downside
; | stack |
; 0x0000 *-------------------*


readdisk:
mov ah, 0x02
mov bx, PROGRAM_SPACE ; read into memory es:bx, es is default 0, so this ok for us to do this
mov al, SECTOR_NUM ; how many sectors do we need to read
mov dl, [BOOT_DISK] ; read from the same driver with ipl's
mov ch, 0x00 ; cylinder 0
mov dh , 0x00 ; surface 0
mov cl , 0x02 ; start reading from the second sector
int 0x13
jc error ; if (read fail) error(); else return;
ret

error:
mov di,errmsg
call printstring
hlt

errmsg:
db 'Disk read failed' , 0
1
2
3
4
$ nasm -o ipl.bin ipl.s
# assemble instruction into machine code
$ dd if=ipl.bin of=floppy.img bs=512 count=1 conv=notrunc
# install into the first sector

3. execute code from second sector, preparing for 32 protected mode

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
; setup.s

[org 0x7e00]

kernel_main equ 0x8000 ; this should be same with the ld -Ttext parameter
; then load the code into memory 0x8000

mov di,greet
call printstring

; change into video mode
mov al,0x13
mov ah,0x00
int 0x10


jmp enterProtected


greet:
db 'In second sector!' , 0

%include "helpfuncs.s"
%include "gdt.s"


; things to do
; 1. disable interrupt, when switching mode, interrupt shouldn't be handled
; 2. enable A20gate to obtain larger memory space
; 3. load gdt into gdtr to enable semgmented memory mangagement
; 4. change ControlRegister0 , cr0
; 5. flush the pipeline with a far jmp, clear those half-way-done 16 bits instructions
; 6. change data segment to 32 bit mode seg
enterProtected:
cli ; 1.
call enableA20 ; 2.
lgdt [gdt_discriptor] ; 3.
call alterCR0 ; 4.
jmp codeseg:start32 ; 5.



enableA20:
in al,0x92
or al,0x02
out 0x92 , al
ret

alterCR0:
mov eax,cr0
or eax,0x0001
mov cr0,eax
ret


[bits 32] ; 32 bits code following, protected mode

start32:
; 6.
mov ax,dataseg
mov ss,ax
mov ds,ax
mov es,ax
mov fs,ax
mov gs,ax
call kernel_main
hlt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
; gdt.s

;gdt entries are stored in GDT
;GDT is stored in memory
;CPU use gdtr to store the size and address of GDT , the value of gdtr is called gdt discriptor

;GDT

;the first segment has to be 0, for some unknown reason
gdt_nullseg:
dd 0
dd 0
;the second segment is code segment
gdt_codeseg:
dw 0xffff ; seg size limit , 20 bits, 16 bits here, 4 bits at the end
dw 0x0000 ; start address of this seg, 32 bits, 24 bits here, 8 bits at the end
db 0x00
db 0b10011010 ; access bits ,8 bits
db 0b11001111 ; G flag, S flag, 00, 4 bits extended seg size limit
db 0x00 ; extended seg start address


;the third segment is data segment
gdt_dataseg:
dw 0xffff
dw 0x0000
db 0x00
db 0b10010010
db 0b11001111
db 0x00

; https://stackoverflow.com/questions/16628800/code-data-segment-overlapping

gdt_end:

gdt_discriptor:
dw gdt_end - gdt_nullseg - 1 ; size = sizeof (GDT) -1 , 16 bits
dd gdt_nullseg ; gdt location, 32 bits



;In protected mode ,the value which is going to be loaded into segment register(ss,ds,...) is the offset in GDT, in other word, an index

; the offsets
codeseg equ gdt_codeseg - gdt_nullseg
dataseg equ gdt_dataseg - gdt_nullseg
1
$ dd if=setup.bin of=floppy.img bs=512 count=1 conv=notrunc seek=1

Beware that this is a cross compilation process, DO NOT use your system compiler(gcc for example)

how to gcc cross compile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# build cross compile env
$ sudo apt update
$ sudo apt install build-essential
$ sudo apt install bison
$ sudo apt install flex
$ sudo apt install libgmp3-dev
$ sudo apt install libmpc-dev
$ sudo apt install libmpfr-dev
$ sudo apt install texinfo
$ cd /tmp/
$ mkdir src
$ cd src
$ curl -O http://ftp.gnu.org/gnu/binutils/binutils-2.35.1.tar.gz
$ tar xzf binutils-2.35.1.tar.gz
$ cd binutils-2.35.1/
$ mkdir -p $HOME/opt/cross
$ export PREFIX="$HOME/opt/cross"
$ export TARGET=i686-elf
$ export PATH="$PREFIX/bin:$PATH"
$ cd ../
$ mkdir build-binutils
$ cd build-binutils/
$ ../binutils-2.35.1/configure --target=$TARGET --prefix="$PREFIX" --with-sysroot --disable-nls --disable-werror | tee config.log
$ make
$ make install
$ cd /tmp/src
$ wget https://ftp.gnu.org/gnu/gcc/gcc-10.2.0/gcc-10.2.0.tar.gz
$ mkdir gcc-build
$ cd gcc-build/
$ which -- $TARGET-as || echo $TARGET-as is not in the PATH
$ cd ..
$ tar xzf gcc-10.2.0.tar.gz
$ cd gcc-build/
$ ../gcc-10.2.0/configure --target=$TARGET --prefix="$PREFIX" --disable-nls --enable-languages=c,c++ --without-headers
$ make all-gcc
$ make all-target-libgcc
$ make install-gcc
$ make install-target-libgcc
$ $HOME/opt/cross/bin/$TARGET-gcc --version
1
2
3
4
5
6
7
8
9
10
11
// kernel.c
void __spoil(void);

void main () {
int i ;
for ( i = 0xa0000 ; i <= 0xaffff ; i++ ){
*((char*)i) = 14 ;
}

__spoil();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
; asmfunc.s
global __spoil

__spoil:
mov edi,0xa0000
next:
mov [edi], byte 13
cmp edi, 0xa0fff
je return
inc edi
jmp next

return:
ret
1
2
$ i686-elf-gcc -ffreestanding -m32 -c -o kernel.o kernel.c
$ i686-elf-ld -Ttext 0x8000 --oformat binary -o kernel.bin kernel.o asmfunc.o

You can see that we only need to obtain raw machine code

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
$  ndisasm -b 32 kernel.bin
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83E4F0 and esp,byte -0x10
00000006 83EC10 sub esp,byte +0x10
00000009 C744240C00000A00 mov dword [esp+0xc],0xa0000
00000011 EB0C jmp short 0x1f
00000013 8B44240C mov eax,[esp+0xc]
00000017 C6000E mov byte [eax],0xe
0000001A 8344240C01 add dword [esp+0xc],byte +0x1
0000001F 817C240CFFFF0A00 cmp dword [esp+0xc],0xaffff
00000027 7EEA jng 0x13
00000029 E812000000 call 0x40
0000002E 90 nop
0000002F C9 leave
00000030 C3 ret
00000031 6690 xchg ax,ax
00000033 6690 xchg ax,ax
00000035 6690 xchg ax,ax
00000037 6690 xchg ax,ax
00000039 6690 xchg ax,ax
0000003B 6690 xchg ax,ax
0000003D 6690 xchg ax,ax
0000003F 90 nop
00000040 BF00000A00 mov edi,0xa0000
00000045 C6070D mov byte [edi],0xd
00000048 81FFFF0F0A00 cmp edi,0xa0fff
0000004E 7403 jz 0x53
00000050 47 inc edi
00000051 EBF2 jmp short 0x45
00000053 C3 ret
$ xxd kernel.bin
00000000: 5589 e583 e4f0 83ec 10c7 4424 0c00 000a U.........D$....
00000010: 00eb 0c8b 4424 0cc6 000e 8344 240c 0181 ....D$.....D$...
00000020: 7c24 0cff ff0a 007e eae8 1200 0000 90c9 |$.....~........
00000030: c366 9066 9066 9066 9066 9066 9066 9090 .f.f.f.f.f.f.f..
00000040: bf00 000a 00c6 070d 81ff ff0f 0a00 7403 ..............t.
00000050: 47eb f2c3 G...

# install to floppy
$ dd if=kernel.bin of=floppy.img bs=512 count=1 conv=notrunc seek=2

5. tidy up things via makefile

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
disk=floppy.img

default: $(disk) ipl.bin setup.bin kernel.bin
dd if=ipl.bin of=$(disk) bs=512 count=1 conv=notrunc
dd if=setup.bin of=$(disk) bs=512 count=1 conv=notrunc seek=1
dd if=kernel.bin of=$(disk) bs=512 count=1 conv=notrunc seek=2

$(disk):
qemu-img create -f raw $(disk) 4M

ipl.bin: ipl.s
nasm -o $@ $?

setup.bin: setup.s
nasm -o $@ $?

asmfunc.o: asmfunc.s
nasm -f elf32 -o $@ $?

kernel.bin: kernel.c asmfunc.o
i686-elf-gcc -ffreestanding -m32 -c -o kernel.o kernel.c
i686-elf-ld -Ttext 0x8000 --oformat binary -o $@ kernel.o asmfunc.o

run:
qemu-system-x86_64 -vga std -drive file=$(disk),format=raw,index=0

clean:
rm -r *.o *.bin *.img