main函数调用之前的代码

编写如下的汇编入口代码, entry.asm

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
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
;彭东 @ 2021.01.09

MBT_HDR_FLAGS EQU 0x00010003
MBT_HDR_MAGIC EQU 0x1BADB002 ;多引导协议头魔数
MBT_HDR2_MAGIC EQU 0xe85250d6 ;第二版多引导协议头魔数
global _start ;导出_start符号
extern main ;导入外部的main函数符号

[section .start.text] ;定义.start.text代码节
[bits 32] ;汇编成32位代码
_start:
jmp _entry
ALIGN 8
mbt_hdr:
dd MBT_HDR_MAGIC
dd MBT_HDR_FLAGS
dd -(MBT_HDR_MAGIC+MBT_HDR_FLAGS)
dd mbt_hdr
dd _start
dd 0
dd 0
dd _entry

;以上是GRUB所需要的头
ALIGN 8
mbt2_hdr:
DD MBT_HDR2_MAGIC
DD 0
DD mbt2_hdr_end - mbt2_hdr
DD -(MBT_HDR2_MAGIC + 0 + (mbt2_hdr_end - mbt2_hdr))
DW 2, 0
DD 24
DD mbt2_hdr
DD _start
DD 0
DD 0
DW 3, 0
DD 12
DD _entry
DD 0
DW 0, 0
DD 8
mbt2_hdr_end:
;以上是GRUB2所需要的头
;包含两个头是为了同时兼容GRUB、GRUB2

ALIGN 8

_entry:
;关中断
cli
;关不可屏蔽中断
in al, 0x70
or al, 0x80
out 0x70,al
;重新加载GDT
lgdt [GDT_PTR]
jmp dword 0x8 :_32bits_mode

_32bits_mode:
;下面初始化C语言可能会用到的寄存器
mov ax, 0x10
mov ds, ax
mov ss, ax
mov es, ax
mov fs, ax
mov gs, ax
xor eax,eax
xor ebx,ebx
xor ecx,ecx
xor edx,edx
xor edi,edi
xor esi,esi
xor ebp,ebp
xor esp,esp
;初始化栈,C语言需要栈才能工作
mov esp,0x9000
;调用C语言函数main
call main
;让CPU停止执行指令
halt_step:
halt
jmp halt_step


GDT_START:
knull_dsc: dq 0
kcode_dsc: dq 0x00cf9e000000ffff
kdata_dsc: dq 0x00cf92000000ffff
k16cd_dsc: dq 0x00009e000000ffff
k16da_dsc: dq 0x000092000000ffff
GDT_END:

GDT_PTR:
GDTLEN dw GDT_END-GDT_START-1
GDTBASE dd GDT_START

因为 需要使用GRUB引导我们的程序,故需要遵循GRUB标准,需要填充两段引导头。

然后关闭终端,设置CPU工作模式。

初始化所有可能用到的寄存器,为C语言提供运行环境

GDT_START是CPU工作模式需要的数据。

进入C语言main函数

1
2
3
4
5
#include "vgastr.h"
void main(){
printf("Hello OS!");
return;
}

可以看出,该main函数是由之前的汇编代码所调用,其中使用printf输出字符串,但是由于无操作系统支持,所以printf需要我们自己手工实现

vgastr.h便是我们手工实现printf函数的头文件,vgastr.c为具体实现

计算机的显卡

计算机显卡统一支持vesa标准,该标准制定了显卡的两种工作模式,分别为字符模式与图形模式。

我们把屏幕分成25行,每行80字符,即(80*25)个位置映射到以0xb8000开头的内存中,每个字节两字符,前者为ascii码,后者为颜色控制字节。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
void _strwrite(char *str){
char *p_dist = (char*)(0xb8000);
while(*str){
*p_dist = *str;
p_dist += 2;
str++;
}
return;
}

void printf(char *fmt, ...){
_strwrite(fmt);
    return;
}

头文件编写如下,

1
2
void _strwrite(char* string);
void printf(char* fmt, ...);

编译Hello OS

编写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
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
MAKEFLAGS = -sR
MKDIR = mkdir
RMDIR = rmdir
CP = cp
CD = cd
DD = dd
RM = rm

ASM = nasm
CC = gcc
LD = ld
OBJCOPY = objcopy

ASMBFLAGS = -f elf -w-orphan-labels
CFLAGS = -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable
LDFLAGS = -s -static -T hello.lds -n -Map HelloOS.map
OJCYFLAGS = -S -O binary

HELLOOS_OBJS :=
HELLOOS_OBJS += entry.o main.o vgastr.o
HELLOOS_ELF = HelloOS.elf
HELLOOS_BIN = HelloOS.bin

.PHONY : build clean all link bin

all: clean build link bin

clean:
$(RM) -f *.o *.bin *.elf

build: $(HELLOOS_OBJS)

link: $(HELLOOS_ELF)
$(HELLOOS_ELF): $(HELLOOS_OBJS)
$(LD) $(LDFLAGS) -o $@ $(HELLOOS_OBJS)
bin: $(HELLOOS_BIN)
$(HELLOOS_BIN): $(HELLOOS_ELF)
$(OBJCOPY) $(OJCYFLAGS) $< $@

%.o : %.asm
$(ASM) $(ASMBFLAGS) -o $@ $<
%.o : %.c
$(CC) $(CFLAGS) -o $@ $<

使用make -n命令可以使make只显示命令而不真实执行

1
2
3
4
5
6
7
8
9
10
11
rm -f *.o *.bin *.elf

nasm -f elf -w-orphan-labels -o entry.o entry.asm

gcc -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable -o main.o main.c

gcc -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable -o vgastr.o vgastr.c

ld -s -static -T hello.lds -n -Map HelloOS.map -o HelloOS.elf entry.o main.o vgastr.o

objcopy -S -O binary HelloOS.elf HelloOS.bin

上述命令便可看出这个Makefile的all目标究竟会干什么

  1. 删除所有以o,bin,elf后缀的文件

  2. 编译汇编代码

  3. 编译C代码

  4. 链接目标文件生成elf文件

  5. elf文件转原始binary二进制文件

由于本人暂时不太懂makefile的具体语法,暂时写了如下python代码作为编译脚本

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
import os

def call(cmd):
print(cmd)
return os.system(cmd)

def clean():
call('rm -f *.o *.bin *.elf')

def build_asm(filename):
call(f'nasm -f elf -w-orphan-labels -o {filename}.o {filename}.asm')

def build_c(filename):
call(f'gcc -c -Os -std=c99 -m32 -Wall -Wshadow -W -Wconversion -Wno-sign-conversion -fno-stack-protector -fomit-frame-pointer -fno-builtin -fno-common -ffreestanding -Wno-unused-parameter -Wunused-variable -o {filename}.o {filename}.c')

def ld(filenames):
file_list = ' '.join(map(lambda x:f'{x}.o',filenames))
call(f'ld -s -static -T hello.lds -n -Map HelloOS.map -o HelloOS.elf {file_list}')

def objcopy():
call('objcopy -S -O binary HelloOS.elf HelloOS.bin')



def build(filenames):
def ns_list():
return map(lambda x:x.split('.'),filenames)

for name,suffix in ns_list():
if suffix == 'c':
build_c(name)
elif suffix == 'asm':
build_asm(name)
else:
print(f'Unsupported file {name}.{suffix}')
ld(map(lambda x:x[0],ns_list()))
objcopy()

filenames = [
'entry.asm',
'main.c',
'vgastr.c',
]
build(filenames)

编译完成后,我们得到了最终的文件HelloOS.bin

安装运行Hello OS

为了方便起见,我们的安装将在虚拟PC中完成,将安装在虚拟硬盘上。

参考这篇博客

创造一块虚拟硬盘 | Zhangzqs

现在我们需要手动让grub引导到我们的HelloOS.bin中。