fix dependency check logic cause config always disabled
[lunaix-os.git] / docs / tutorial / 1-hello_kernel_world.md
1 ## 准备工作
2
3 首先clone仓库,回滚到下面的commit。如果想编译这个版本的代码建议还是使用自行编译gcc编译器。
4
5 ```sh
6 git checkout e0ee3d449aacd33a84cb1f58961e55f9f06acb46
7 ```
8
9 除此之外读者还需要准备有makefile的基础。
10
11 ## 项目结构
12
13 - makefile:用于编译
14 - linker.ld:用于链接
15 - 其他:主要是内核代码
16
17 我们先理清楚从项目到镜像的生成过程:
18
19 1. 根据makefile中的描述编译各个子文件,得到object文件(参见build/obj文件夹里面的文件),之后对这些子文件的编译后的文件调用链接
20 2. 根据linker.ld对这些object文件进行链接,得到lunaix.bin
21 3. 使用grub-mkrescue,结合lunaix.bin来制作lunaix.iso
22
23 上面三个过程在makefile中都有体现
24
25 1.
26
27 ```makefile
28 SOURCE_FILES := $(shell find -name "*.[cS]")
29 SRC := $(patsubst ./%, $(OBJECT_DIR)/%.o, $(SOURCE_FILES))
30
31 $(OBJECT_DIR):
32         @mkdir -p $(OBJECT_DIR)
33
34 $(BIN_DIR):
35         @mkdir -p $(BIN_DIR)
36
37 $(ISO_DIR):
38         @mkdir -p $(ISO_DIR)
39         @mkdir -p $(ISO_BOOT_DIR)
40         @mkdir -p $(ISO_GRUB_DIR)
41
42 $(OBJECT_DIR)/%.S.o: %.S
43         @mkdir -p $(@D)
44         $(CC) -c $< -o $@
45 ......
46 ```
47
48 2.
49
50 ```makefile
51 $(BIN_DIR)/$(OS_BIN): $(OBJECT_DIR) $(BIN_DIR) $(SRC)
52         $(CC) -T linker.ld -o $(BIN_DIR)/$(OS_BIN) $(SRC) $(LDFLAGS)
53 ```
54
55 3.
56
57 ```makefile
58 $(BUILD_DIR)/$(OS_ISO): $(ISO_DIR) $(BIN_DIR)/$(OS_BIN) GRUB_TEMPLATE
59         @./config-grub.sh ${OS_NAME} > $(ISO_GRUB_DIR)/grub.cfg
60         @cp $(BIN_DIR)/$(OS_BIN) $(ISO_BOOT_DIR)
61         @grub-mkrescue -o $(BUILD_DIR)/$(OS_ISO) $(ISO_DIR)
62 ```
63
64 ## 步骤一
65
66 makefile中最开始是指定了一些目录。
67
68 ```makefile
69 OS_ARCH := x86
70
71 BUILD_DIR := build
72 KERNEL_DIR := kernel
73 OBJECT_DIR := $(BUILD_DIR)/obj
74 BIN_DIR := $(BUILD_DIR)/bin
75 ISO_DIR := $(BUILD_DIR)/iso
76 ISO_BOOT_DIR := $(ISO_DIR)/boot
77 ISO_GRUB_DIR := $(ISO_BOOT_DIR)/grub
78 ```
79
80 下面是把第三个参数根据第一个参数的匹配模式替换成第二个参数,这里就是通过第一个参数`%`(匹配任意字符串)匹配到`includes`最后替换成`-Iincludes`
81
82 ```makefile
83 INCLUDES_DIR := includes
84 INCLUDES := $(patsubst %, -I%, $(INCLUDES_DIR))
85 ```
86
87 不清楚的可以在下面添加打印命令,查看patsubst处理的结果
88
89 ```makefile
90 $(OBJECT_DIR):
91         @echo "================="
92         @echo $(INCLUDES);
93         @echo "================="
94         ......
95 ```
96
97 接下来是一些名称的定义
98
99 ```makefile
100 OS_NAME = lunaix
101 OS_BIN = $(OS_NAME).bin
102 OS_ISO = $(OS_NAME).iso
103
104 CC := i686-elf-gcc
105 AS := i686-elf-as
106
107 O := -O3
108 W := -Wall -Wextra
109 CFLAGS := -std=gnu99 -ffreestanding $(O) $(W)
110 LDFLAGS := -ffreestanding $(O) -nostdlib -lgcc
111 ```
112
113 执行shell命令,做到所有后缀为.c或者.S文件的全部文件名词。就是搜集所有c代码文件和汇编代码文件的名称。
114
115 ```makefile
116 SOURCE_FILES := $(shell find -name "*.[cS]")
117 ```
118
119 同样可以修改makefile,运行`make all`来打印`SOURCE_FILES`结果,结果如下
120
121 ```bash
122 ====================
123 ./kernel/tty/tty.c ./kernel/kernel.c ./arch/x86/boot.S
124 ====================
125 ```
126
127 创建上面指定的文件对应的文件夹
128
129 ```makefile
130 $(OBJECT_DIR):
131         @mkdir -p $(OBJECT_DIR)
132
133 $(BIN_DIR):
134         @mkdir -p $(BIN_DIR)
135
136 $(ISO_DIR):
137         @mkdir -p $(ISO_DIR)
138         @mkdir -p $(ISO_BOOT_DIR)
139         @mkdir -p $(ISO_GRUB_DIR)
140 ```
141
142 把汇编文件和c源代码文件编译成object文件,可以看到`$(INCLUDES)`作用就是指定头文件的文件夹路径
143
144 ```makefile
145 $(OBJECT_DIR)/%.S.o: %.S
146         @mkdir -p $(@D)
147         $(CC) -c $< -o $@
148
149 $(OBJECT_DIR)/%.c.o: %.c 
150         @mkdir -p $(@D)
151         $(CC) $(INCLUDES) -c $< -o $@ $(CFLAGS)
152 ```
153
154 ## 步骤二
155
156 根据linker.ld来进行链接
157
158 ```makefile
159 $(BIN_DIR)/$(OS_BIN): $(OBJECT_DIR) $(BIN_DIR) $(SRC)
160         $(CC) -T linker.ld -o $(BIN_DIR)/$(OS_BIN) $(SRC) $(LDFLAGS)
161 ```
162
163 下面分析一下linker.ld
164
165 ```
166 ENTRY(start_)
167
168 SECTIONS {
169     . = 0x100000;
170
171     .text BLOCK(4K) : {
172         * (.multiboot)
173         * (.text)
174     }
175
176     .bss BLOCK(4K) : {
177         * (COMMON)
178         * (.bss)
179     }
180
181     .data BLOCK(4k) : {
182         * (.data)
183     }
184
185     .rodata BLOCK(4K) : {
186         * (.rodata)
187     }
188 }
189 ```
190
191 先是指明了入口符号是start_,这个其实是一个地址,后面会看到。
192
193 `. = 0x100000`表示起始地址为0x100000。后面.text的地址就是从0x100000开始的。随后`.bss`就是从.text结束的地址再进行对齐计算得到的地址开始的。
194
195 之后将所有object文件的节进行分配。比如把所有object文件的`.data`节的内容汇总放入到lunaix.bin的`.data`节中。COMMON代表一些未初始化的全局变量。总之lunaix.bin的节是可以自定义的,后面也会添加一些自己命名的节。`.text BLOCK(4K)`表示`.text`的地址是4K对齐的。
196
197 下面是一个object文件的部分节信息
198
199 ```sh
200 $ readelf -S kernel.c.o 
201 There are 21 section headers, starting at offset 0x6fc:
202
203 Section Headers:
204   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
205   [ 0]                   NULL            00000000 000000 000000 00      0   0  0
206   [ 1] .text             PROGBITS        00000000 000034 000029 00  AX  0   0  1
207   [ 2] .rel.text         REL             00000000 00054c 000018 08   I 18   1  4
208   [ 3] .data             PROGBITS        00000000 00005d 000000 00  WA  0   0  1
209   [ 4] .bss              NOBITS          00000000 00005d 000000 00  WA  0   0  1
210   [ 5] .rodata           PROGBITS        00000000 000060 000029 00   A  0   0  4
211   ......
212 ```
213
214 下面是lunaix.bin的部分节信息,可以看到结果正如在linker.ld中规划的那样。都是4K对齐的。一个页的大小也是4KB,一般是要防止两个节放入同一个页。
215
216 ```sh
217 $ readelf -S lunaix.bin 
218 There are 16 section headers, starting at offset 0x3b54:
219
220 Section Headers:
221   [Nr] Name              Type            Addr     Off    Size   ES Flg Lk Inf Al
222   [ 0]                   NULL            00000000 000000 000000 00      0   0  0
223   [ 1] .text             PROGBITS        00100000 001000 0001e9 00  AX  0   0  1
224   [ 2] .bss              NOBITS          00101000 002000 003fce 00  WA  0   0 16
225   [ 3] .data             PROGBITS        00105000 002000 000004 00  WA  0   0  4
226   [ 4] .rodata           PROGBITS        00106000 003000 000029 00   A  0   0  4
227   ......
228 ```
229
230 ## 步骤三
231
232 制作ISO文件
233
234 ```makefile
235 $(BUILD_DIR)/$(OS_ISO): $(ISO_DIR) $(BIN_DIR)/$(OS_BIN) GRUB_TEMPLATE
236         @./config-grub.sh ${OS_NAME} > $(ISO_GRUB_DIR)/grub.cfg
237         @cp $(BIN_DIR)/$(OS_BIN) $(ISO_BOOT_DIR)
238         @grub-mkrescue -o $(BUILD_DIR)/$(OS_ISO) $(ISO_DIR)
239 ```
240
241 把上面的`@`去掉,可以知道执行了什么命令。
242
243 ```
244 ./config-grub.sh lunaix > build/iso/boot/grub/grub.cfg
245 cp build/bin/lunaix.bin build/iso/boot
246 grub-mkrescue -o build/lunaix.iso build/iso
247 ```
248
249 先是执行下面脚本,参数$1就是lunaix,结果重定向到build/iso/boot/grub/grub.cfg文件。
250
251 ```sh
252 #!/usr/bin/bash
253
254 export _OS_NAME=$1
255
256 echo $(cat GRUB_TEMPLATE | envsubst)
257 ```
258
259 尝试运行下面命令,可以知道我们要提供`$_OS_NAME`的值。那么上面的第一行就是用于提供值$1,也就是命令行的参数lunaix。`envsubset`会把`$_OS_NAME`的值替换成lunaix。
260
261 ```sh
262 $ cat GRUB_TEMPLATE
263 menuentry "$_OS_NAME" {
264         multiboot /boot/$_OS_NAME.bin
265 }
266 ```
267
268 build/iso/boot/grub/grub.cfg文件内容和预期一样。在multiboot后面指定bin的路径即可。这个grub.cfg也是可以自定义的。
269
270 ```
271 menuentry "lunaix" { multiboot /boot/lunaix.bin }
272 ```
273
274 grub-mkrescue会根据grub.cfg来制作ISO文件。制作后会放入自动生成的bootloader,所以我们不需要写bootloader。只需要从入口点开始写代码。
275
276 大概框架就是这样,具体细节之后会学习到。
277
278 ## 内核代码分析
279
280 ### arch/x86/boot.S
281
282 先看.text节的内容
283
284 ```assembly
285 .section .text
286     .global start_
287     .type start_, @function
288     start_:
289         movl $stack_top, %esp
290         /* 
291             TODO: kernel init
292                 1. Load GDT
293                 2. Load IDT
294                 3. Enable paging
295         */
296         call _kernel_init
297
298         pushl %ebx
299         call _kernel_main
300
301         cli
302     j_:
303         hlt
304         jmp j_
305 ```
306
307 start_就是链接文件里面提到的ENTRY,引导程序会引导到这个指定的入口。
308
309 伪指令.global声明_start为全局符号。
310
311 下面先介绍一下符号。
312
313 先准备好两个文件:`a.c`、`a2.c`。
314
315 `a.c`
316
317 ```c
318 #include <stdio.h>
319 int main()
320 {
321     printf("address:%lx\n", &func);
322     return 0;
323 }
324 ```
325
326 `a2.c`
327
328 ```c
329 #include <stdio.h>
330 extern void func();
331 int main()
332 {
333     printf("address:%lx\n", &func);
334     return 0;
335 }
336 ```
337
338 分别把a.c和a2.c编译成object文件,会发现前者无法通过编译,后者可以。
339
340 ```sh
341 $ gcc -m32 -c a.c -o a.o
342 a.c: In function ‘main’:
343 a.c:5:28: error: ‘func’ undeclared (first use in this function)
344     5 |     printf("address:%lx", &func);
345       |                            ^~~~
346 a.c:5:28: note: each undeclared identifier is reported only once for each function it appears in
347 ```
348
349 ```sh
350 $ gcc -m32 -c a2.c -o a2.o
351 a2.c: In function ‘main’:
352 a2.c:5:23: warning: format ‘%lx’ expects argument of type ‘long unsigned int’, but argument 2 has type ‘void (*)()’ [-Wformat=]
353     5 |     printf("address:%lx\n", &func);
354       |                     ~~^     ~~~~~
355       |                       |     |
356       |                       |     void (*)()
357       |                       long unsigned int
358 ```
359
360 这就涉及到符号的概念。这里的函数是一个符号,在符号未声明时,是无法通过编译的。`extern void func();`就是用来对符号声明。使用extern就是让编译器放心,符号在其他对象文件存在。假如其他文件也没有符号,链接器就会报错了。这个时候需要检查符号是否存在。
361
362 声明为全局符号相当于成为符号供应方,这样其他符号需求方才能成功链接。
363
364 如果其他文件里面要jump到这个_start,链接时会从全局符号里面看是否存在这个符号,如果存在,则使用全局符号的地址。总之,如果要在其他文件使用这个文件的函数,需要声明成全局的。_
365
366 `.type start_, @function`声明为函数。
367
368 里面简单的初始化了esp栈顶,调用了\_kernel_init和_kernel_main。
369
370 \_kernel_init还没有代码,_kernel_main用于打印信息。
371
372 最后就是一个死循环,防止退出。
373
374 之后来看看.multiboot节[1]
375
376 ```assembly
377 .section .multiboot
378     .long MB_MAGIC
379     .long MB_ALIGNED_4K_MEM_MAP
380     .long CHECKSUM(MB_ALIGNED_4K_MEM_MAP)
381 ```
382
383 第一个.long表示在节的第一个32bits中,存储MB_MAGIC(0x1BADB002)。第三个用于配置一些选项(0x3表示在页面边界加载模块和提供内存地图)。第三个用于校验。
384
385 其他类推,可以打开二进制编辑器来验证。这个节的作用就是为了满足约定。为了让GRUB 能够识别镜像文件,我们需要硬编码。最好像链接脚本那样把.multiboot放到第一个位置[2]。
386
387 ### kernel/tty/tty.c
388
389 根据视频中提到的文档定义宽度和长度,表示每行80个字符,总共25行(VGA文本模式)。buffer指向的是一个固定的地址[3],也是文档定义的。操作这个地址才能在屏幕上打印出字符。最后是两个表示当前位置的全局变量。
390
391 ```c
392 #define TTY_WIDTH 80
393 #define TTY_HEIGHT 25
394
395 vga_atrributes *buffer = 0xB8000;
396
397 vga_atrributes theme_color = VGA_COLOR_BLACK;
398
399 uint32_t TTY_COLUMN = 0;
400 uint16_t TTY_ROW = 0;
401 ```
402
403 根据文档设置background color和foreground color。unsigned short两个字节,bg和fg占高8位(设置颜色),低八位(一个字节)用于存储字符信息。
404
405 ```c
406 typedef unsigned short vga_atrributes; 
407
408 void tty_set_theme(vga_atrributes fg, vga_atrributes bg) {
409     theme_color = (bg << 4 | fg) << 8;
410 }
411 ```
412
413 `tty_put_char`实现了字符打印。如果输入字符是`\n`则把行数加1,如果是`\r`则把光标移动到行头部。
414
415 ```c
416 void tty_put_char(char chr) {
417     if (chr == '\n') {
418         TTY_COLUMN = 0;
419         TTY_ROW++;
420     }
421     else if (chr == '\r') {
422         TTY_COLUMN = 0;
423     }
424     else {
425         *(buffer + TTY_COLUMN + TTY_ROW * TTY_WIDTH) = (theme_color | chr);
426         TTY_COLUMN++;
427         if (TTY_COLUMN >= TTY_WIDTH) {
428             TTY_COLUMN = 0;
429             TTY_ROW++;
430         }
431     }
432
433     if (TTY_ROW >= TTY_HEIGHT) {
434         tty_scroll_up();
435         TTY_ROW--;
436     } 
437 }
438 ```
439
440 下面是真正写入字符的一行语句,高八位是颜色信息,低八位是`chr`。
441
442 ```c
443 *(buffer + TTY_COLUMN + TTY_ROW * TTY_WIDTH) = (theme_color | chr);
444 ```
445
446 `tty_scroll_up`在屏幕满的时候被调用,用于滚动屏幕,该函数暂时未实现。其他情况也是很容易看懂的。
447
448 ## 参考
449
450 [1]https://wiki.osdev.org/Multiboot#Header_Format
451
452 [2]https://wiki.osdev.org/User:Zesterer/Bare_Bones#kernel.c
453
454 [3]https://en.wikipedia.org/wiki/VGA_text_mode#cite_note-cyrix-14:~:text=The%20VGA%20text%20buffer%20is%20located%20at%20physical%20memory%20address%200xB8000.