一、整理下到目前为止的流程图

写到这,终于才把一些苦力活都干完了,也终于到了我们的内核代码部分,也终于开始第一次用 c 语言写代码了!为了这个阶段性的胜利,以及更好地进入内核部分,下图贴一张到目前为止的流程图。(其中黄色部分是今天准备做的事情)

二、先上代码

loader.asm

  1. ...
  2. ;加载kernel
  3. mov eax,0x9 ;kernel.bin所在的扇区号 0x9
  4. mov ebx,0x70000 ;写入的内存地址 0x70000
  5. mov ecx,200 ;读入的扇区数
  6. call rd_disk_m_32
  7. ...
  8. ;进入内核
  9. call kernel_init
  10. mov byte [gs:0x280],'i'
  11. mov byte [gs:0x282],'n'
  12. mov byte [gs:0x284],'i'
  13. mov byte [gs:0x286],'t'
  14. mov byte [gs:0x28a],'k'
  15. mov byte [gs:0x28c],'e'
  16. mov byte [gs:0x28e],'r'
  17. mov byte [gs:0x290],'n'
  18. mov byte [gs:0x292],'e'
  19. mov byte [gs:0x294],'l'
  20. mov esp,0xc009f000
  21. jmp 0xc0001500
  22. ; kernel.bin中的segment拷贝到编译的地址
  23. kernel_init:
  24. xor eax,eax
  25. xor ebx,ebx ;记录程序头表地址(内核地址+程序头表偏移地址)
  26. xor ecx,ecx ;记录程序头中的数量
  27. xor edx,edx ;记录程序头表中每个条目的字节大小
  28. mov dx,[0x70000+42] ;偏移文件42字节处是e_phentsize
  29. mov ebx,[0x70000+28] ;偏移文件28字节处是e_phoff
  30. add ebx,0x70000
  31. mov cx,[0x70000+44] ;偏移文件44字节处是e_phnum
  32. .each_segment:
  33. cmp byte [ebx+0],0 ;p_type=0,说明此头未使用
  34. je .PTNULL
  35. push dword [ebx+16] ;p_filesz压入栈(mem_cpy第三个参数)
  36. mov eax,[ebx+4]
  37. add eax,0x70000
  38. push eax ;p_offset+内核地址=段地址(mem_cpy第二个参数)
  39. push dword [ebx+8] ;p_vaddr(mem_cpy第一个参数)
  40. call mem_cpy
  41. add esp,12
  42. .PTNULL:
  43. add ebx,edx ;ebx指向下一个程序头
  44. loop .each_segment
  45. ret
  46. ;主子拷贝函数(dst,src,size
  47. mem_cpy:
  48. cld
  49. push ebp
  50. mov ebp,esp
  51. push ecx
  52. mov edi,[ebp+8] ;dst
  53. mov esi,[ebp+12] ;src
  54. mov ecx,[ebp+16] ;size
  55. rep movsb
  56. pop ecx
  57. pop ebp
  58. ret
  59. ; 以下是两个函数的具体实现,不看不影响理解主流程
  60. ; 保护模式的硬盘读取函数
  61. rd_disk_m_32:
  62. mov esi, eax
  63. mov di, cx
  64. mov dx, 0x1f2
  65. mov al, cl
  66. out dx, al
  67. mov eax, esi
  68. ; 保存LBA地址
  69. mov dx, 0x1f3
  70. out dx, al
  71. mov cl, 8
  72. shr eax, cl
  73. mov dx, 0x1f4
  74. out dx, al
  75. shr eax, cl
  76. mov dx, 0x1f5
  77. out dx, al
  78. shr eax, cl
  79. and al, 0x0f
  80. or al, 0xe0
  81. mov dx, 0x1f6
  82. out dx, al
  83. mov dx, 0x1f7
  84. mov al, 0x20
  85. out dx, al
  86. .not_ready:
  87. nop
  88. in al, dx
  89. and al, 0x88
  90. cmp al, 0x08
  91. jnz .not_ready
  92. mov ax, di
  93. mov dx, 256
  94. mul dx
  95. mov cx, ax
  96. mov dx, 0x1f0
  97. .go_on_read:
  98. in ax, dx
  99. mov [ds:ebx], ax
  100. add ebx, 2
  101. loop .go_on_read
  102. ret

main.c

  1. #include "print.h"
  2. int main(void){
  3. put_str("put_str finish\n");
  4. while(1);
  5. return 0;
  6. }

print.h

  1. #ifndef __LIB_KERNEL_PRINT_H
  2. #define __LIB_KERNEL_PRINT_H
  3. #include "stdint.h"
  4. void put_char(uint8_t char_asci);
  5. void put_str(char* message);
  6. #endif

print.asm

  1. TI_GDT equ 0
  2. RPL0 equ 0
  3. SELECTOR_VIDEO equ (0x0003<<3)+TI_GDT+RPL0
  4. [bits 32]
  5. section .text
  6. global put_str
  7. put_str:
  8. push ebx
  9. push ecx
  10. xor ecx,ecx
  11. mov ebx,[esp+12]
  12. .goon:
  13. mov cl,[ebx]
  14. cmp cl,0
  15. jz .str_over
  16. push ecx
  17. call put_char
  18. add esp,4
  19. inc ebx
  20. jmp .goon
  21. .str_over:
  22. pop ecx
  23. pop ebx
  24. ret
  25. global put_char
  26. put_char:
  27. pushad
  28. ;保证gs中为正确到视频段选择子
  29. mov ax,SELECTOR_VIDEO
  30. mov gs,ax
  31. ;获取当前光标位置
  32. ;获得高8
  33. mov dx,0x03d4 ;索引寄存器
  34. mov al,0x0e
  35. out dx,al
  36. mov dx,0x03d5
  37. in al,dx
  38. mov ah,al
  39. ;获得低8
  40. mov dx,0x03d4
  41. mov al,0x0f
  42. out dx,al
  43. mov dx,0x03d5
  44. in al,dx
  45. ;将光标存入bx
  46. mov bx,ax
  47. mov ecx,[esp+36]
  48. cmp cl,0xd
  49. jz .is_carriage_return
  50. cmp cl,0xa
  51. jz .is_line_feed
  52. cmp cl,0x8
  53. jz .is_backspace
  54. jmp .put_other
  55. .is_backspace:
  56. dec bx
  57. shl bx,1
  58. mov byte [gs:bx],0x20
  59. inc bx
  60. mov byte [gs:bx],0x07
  61. shr bx,1
  62. jmp .set_cursor
  63. .put_other:
  64. shl bx,1
  65. mov [gs:bx],cl
  66. inc bx
  67. mov byte [gs:bx],0x07
  68. shr bx,1
  69. inc bx
  70. cmp bx,2000
  71. jl .set_cursor
  72. .is_line_feed:
  73. .is_carriage_return:
  74. ;cr(\r),只要把光标移到首行就行了
  75. xor dx,dx
  76. mov ax,bx
  77. mov si,80
  78. div si
  79. sub bx,dx
  80. .is_carriage_return_end:
  81. add bx,80
  82. cmp bx,2000
  83. .is_line_feed_end:
  84. jl .set_cursor
  85. .roll_screen:
  86. cld
  87. mov ecx,960
  88. mov esi,0xc00b80a0 ;第1行行首
  89. mov edi,0xc00b8000 ;第0行行首
  90. rep movsd
  91. ;最后一行填充为空白
  92. mov ebx,3840
  93. mov ecx,80
  94. .cls:
  95. mov word [gs:ebx],0x0720
  96. add ebx,2
  97. loop .cls
  98. mov bx,1920 ;最后一行行首
  99. .set_cursor:
  100. ;将光标设为bx
  101. ;设置高8
  102. mov dx,0x03d4
  103. mov al,0x0e
  104. out dx,al
  105. mov dx,0x03d5
  106. mov al,bh
  107. out dx,al
  108. ;再设置低8
  109. mov dx,0x03d4
  110. mov al,0x0f
  111. out dx,al
  112. mov dx,0x03d5
  113. mov al,bl
  114. out dx,al
  115. .put_char_done:
  116. popad
  117. ret

Makefile

  1. mbr.bin: mbr.asm
  2. nasm -I include/ -o out/mbr.bin mbr.asm -l out/mbr.lst
  3. loader.bin: loader.asm
  4. nasm -I include/ -o out/loader.bin loader.asm -l out/loader.lst
  5. kernel.bin: kernel/main.c
  6. nasm -f elf -o out/print.o lib/kernel/print.asm
  7. gcc -I lib/kernel/ -c -o out/main.o kernel/main.c
  8. ld -Ttext 0xc0001500 -e main -o out/kernel.bin out/main.o out/print.o
  9. os.raw: mbr.bin loader.bin kernel.bin
  10. ../bochs/bin/bximage -hd -mode="flat" -size=60 -q target/os.raw
  11. dd if=out/mbr.bin of=target/os.raw bs=512 count=1
  12. dd if=out/loader.bin of=target/os.raw bs=512 count=4 seek=2
  13. dd if=out/kernel.bin of=target/os.raw bs=512 count=200 seek=9
  14. brun:
  15. make install
  16. make only-bochs-run
  17. only-bochs-run:
  18. ../bochs/bin/bochs -f ../bochs/bochsrc.disk -q
  19. install:
  20. make clean
  21. make -r os.raw

三、鸟瞰代码

  1. ;加载kernel
  2. mov eax,0x9 ;kernel.bin所在的扇区号 0x9
  3. mov ebx,0x70000 ;写入的内存地址 0x70000
  4. mov ecx,200 ;读入的扇区数
  5. call rd_disk_m_32
  6. ;进入内核
  7. call kernel_init
  8. mov esp,0xc009f000
  9. jmp 0xc0001500

我将关键部分提取出来,有助于你鸟瞰本讲的全部代码要做的事。本段代码实际上就做了这么几个事:

  1. 将硬盘第 9 扇区开始后的 200 个扇区的内容(包括 kernel.bin),复制到内存 0x70000 开始的地方
  2. call kernel_init 调用了一下这个方法,这个方法干嘛之后再说,也是重点
  3. 栈指针赋值为 0xc009f000,并跳转到 0xc0001500 开始执行

有一点有些不符合我们的直觉,既然 kernel.bin 被写入内存第 0x70000 位置了,按照我们之前一跳二跳三跳的写法,应该直接跳转到 0x70000,可为什么是 0xc0001500 呢?

下面直接解答这个问题,

kernel.bin 是用 c 语言 写好之后编译出来的产物,不像之前我们都是直接汇编语言 .asm 编译成 .bin。c 语言在 linux 的 gcc 工具编译后的二进制文件,是一个格式为 ELF 的文件,并不完全是从头到尾都是可执行的机器指令。

这个格式里肯定有某个地方指出,指令代码在什么位置(相对文件开始的偏移量),并且要求加载这种格式文件的程序(kernel_init),将指令代码放在内存中的什么位置(0xc0001500)。

如果是这样的话,整个流程就说通了,kernel_init 只是将 kernel.bin 这个 ELF 格式的文件里的关键信息提取出来,最重要的就是加载到内存中的什么位置这个信息,然后执行相应的处理操作。

那接下来,我们就该详细看看,ELF 格式究竟是什么?

四、详解 ELF 格式

ELF:1999 年,被 86open 项目选为 x86 架构上的类 Unix 操作系统的二进制文件标准格式,用来取代 COFF,也是 Linux 的主要可执行文件格式

为什么要有这种格式呢?其实没有这种格式也是完全可以的,但我们用户写的应用程序,是独立与操作系统之外的。换句话说,就是需要操作系统这个 主应用程序,去调用那些用户写出来的 应用程序。如果没有一种特定的格式当然也可以,那就让操作系统约定俗成一个内存地址来存放用户的应用程序,这样应用程序也不能将自己的程序分成一段一段的。所以有个格式,至少是只有好处没有坏处。

刚刚只提到了可执行文件,生成可执行文件之前还要经历一个重定位文件的过程,链接之后才是可执行文件。重定位文件可执行文件都可以用 ELF 格式来表示,该格式有一个统一的,下面分成好多个和好多个,多个节通过链接变成一个段,具体格式如下图。

ELF 格式鸟瞰

ELF 格式具体定义

先定义下数据类型方便后续描述

数据类型 字节大小
Elf32_Half 无符号整数(2)
Elf32_Word 无符号整数(4)
Elf32_Addr 程序运行地址(4)
Elf32_Off 文件偏移量(4)

ELF 头

数据类型 名称 字节 含义 例子
unsigned char e_ident[16] 16 0-3魔数 4类型 5大小端 6版本 7-15保留零
Elf32_Half e_type 2 文件类型:0未知 1可重定位 2可执行 3动态共享目标 4core 0x0002
Elf32_Half e_machine 2 处理器结构:0未知 3Intel80386 8MIPSRS3000 0x0003
Elf32_Word e_version 4 版本 0x00000001
Elf32_Addr e_entry 4 用来指明操作系统运行该程序时,将控制权转交到的虚拟地址 0xc0001500
Elf32_Off e_phoff 4 程序头表(program header table)在文件内的字节偏移量。没有为0 0x00000034
Elf32_Off e_shoff 4 节头表(section header table)在文件内的字节偏移量。没有为0 0x0000055c
Elf32_Word e_flags 4 与处理器相关标志 0x00000000
Elf32_Half e_enhsize 2 elf header的字节大小 0x0034
Elf32_Half e_phentsize 2 程序头表(program header table)中每个条目(entry)的字节大小 0x0020
Elf32_Half e_phnum 2 程序头表中条目的数量。实际上就是段的个数 0x0002
Elf32_Half e_shentsize 2 节头表(section header table)中每个条目(entry)的字节大小 0x0028
Elf32_Half e_shnum 2 程序头表中条目的数量。实际上就是节的个数 0x0006
Elf32_Half e_shstmdx 2 用来指明string name table在节头表中的索引index 0x0003

程序头表

数据类型 名称 字节 含义 例子
Elf32_Word p_type 4 段的类型:1可加载的程序段 2动态连接信息 3动态加载器名称 0x00000001
Elf32_Off p_offset 4 本段在文件内的起始偏移字节 0x00000000
Elf32_Addr p_vaddr 4 本段在内存中的起始虚拟地址 0xc0001000
Elf32_Addr p_paddr 4 物理地址相关,保留,未设定 0xc0001000
Elf32_Word p_filesz 4 本段在文件中的大小 0x0000060b
Elf32_Word p_memsz 4 本段在内存中的大小 0x0000060b
Elf32_Word p_flags 4 标志 1可执行 2可写 4可读 0x00000005
Elf32_Word p_align 4 对其方式 0不对齐 2的幂次对齐 0x00001000

其实不用想得多复杂,就是一个格式而已,程序中需要哪个数据,就根据偏移量把它取出来用就可以了,实际上我们的程序就是这么做的。

来看一下 kernel.bin 的具体内容

7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00

02 00 03 00 01 00 00 00 [00 15 00 c0] [34 00 00 00]

64 06 00 00 00 00 00 00 34 00 [20 00] [02 00] 28 00

06 00 03 00 01 00 00 00 [00 00 00 00] [00 10 00 c0]

00 10 00 c0 [0b 06 00 00] 0b 06 00 00 05 00 00 00

00 10 00 00 51 e5 74 64 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 07 00 00 00

04 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00

...

按照上述的 ELF 格式表一一对应看,便能知道全部信息,其中我们本次代码中用到的,都用加粗了。我们拿 ELF 文件查看器工具看一下(不是必须的)

代码中的 kernel_init 就是将 ELF 格式文件中的 程序头表地址程序头中的数量程序头表中每个条目的字节大小加载到的内存地址 取出,然后执行相应的拷贝操作。

  1. kernel_init:
  2. xor eax,eax
  3. xor ebx,ebx ;记录程序头表地址(内核地址+程序头表偏移地址)
  4. xor ecx,ecx ;记录程序头中的数量
  5. xor edx,edx ;记录程序头表中每个条目的字节大小
  6. mov dx,[0x70000+42] ;偏移文件42字节处是e_phentsize
  7. mov ebx,[0x70000+28] ;偏移文件28字节处是e_phoff
  8. add ebx,0x70000
  9. mov cx,[0x70000+44] ;偏移文件44字节处是e_phnum
  10. .each_segment:
  11. cmp byte [ebx+0],0 ;p_type=0,说明此头未使用
  12. je .PTNULL
  13. push dword [ebx+16] ;p_filesz压入栈(mem_cpy第三个参数)
  14. mov eax,[ebx+4]
  15. add eax,0x70000
  16. push eax ;p_offset+内核地址=段地址(mem_cpy第二个参数)
  17. push dword [ebx+8] ;p_vaddr(mem_cpy第一个参数)
  18. call mem_cpy
  19. add esp,12
  20. .PTNULL:
  21. add ebx,edx ;ebx指向下一个程序头
  22. loop .each_segment
  23. ret

五、c 语言和汇编语言相互调用

本章讲述了 ELF 格式的可执行文件,还讲述了如何加载一个 ELF 可执行文件,并跳转到相应的地址去执行。

本章还隐含讲述了汇编语言如何调用 c 语言(约定好跳转地址,以及传参方式),以及 C 语言如何调用汇编语言。

c 语言调用汇编

print.asm

  1. global put_str
  2. put_str:
  3. ...
  4. ret

main.c

  1. #include "print.h"
  2. int main(void){
  3. put_str();
  4. return 0;
  5. }

print.h

  1. #ifndef __LIB_KERNEL_PRINT_H
  2. #define __LIB_KERNEL_PRINT_H
  3. void put_str();
  4. #endif

写在最后:开源项目和课程规划

如果你对自制一个操作系统感兴趣,不妨跟随这个系列课程看下去,甚至加入我们(下方有公众号和小助手微信),一起来开发。

参考书籍

《操作系统真相还原》这本书真的赞!强烈推荐

项目开源

项目开源地址:https://gitee.com/sunym1993/flashos

当你看到该文章时,代码可能已经比文章中的又多写了一些部分了。你可以通过提交记录历史来查看历史的代码,我会慢慢梳理提交历史以及项目说明文档,争取给每一课都准备一个可执行的代码。当然文章中的代码也是全的,采用复制粘贴的方式也是完全可以的。

如果你有兴趣加入这个自制操作系统的大军,也可以在留言区留下您的联系方式,或者在 gitee 私信我您的联系方式。

课程规划

本课程打算出系列课程,我写到哪觉得可以写成一篇文章了就写出来分享给大家,最终会完成一个功能全面的操作系统,我觉得这是最好的学习操作系统的方式了。所以中间遇到的各种坎也会写进去,如果你能持续跟进,跟着我一块写,必然会有很好的收货。即使没有,交个朋友也是好的哈哈。

目前的系列包括

【自制操作系统06】终于开始用 C 语言了,第一行内核代码!的更多相关文章

  1. 《30天自制操作系统》笔记(02)——导入C语言

    <30天自制操作系统>笔记(02)——导入C语言 进度回顾 在上一篇,记录了计算机开机时加载IPL程序(initial program loader,一个nas汇编程序)的情况,包括IPL ...

  2. 《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!

    <30天自制操作系统>笔记(01)——hello bitzhuwei's OS! 最初的OS代码 ; hello-os ; TAB=4 ORG 0x7c00 ; 指明程序的装载地址 ; 以 ...

  3. 《30天自制操作系统》笔记(01)——hello bitzhuwei’s OS!【转】

    转自:http://www.cnblogs.com/bitzhuwei/p/OS-in-30-days-01-hello-bitzhuwei-OS.html 阅读目录(Content) 最初的OS代码 ...

  4. 《30天自制操作系统》笔记(06)——CPU的32位模式

    <30天自制操作系统>笔记(06)——CPU的32位模式 进度回顾 上一篇中实现了启用鼠标.键盘的功能.屏幕上会显示出用户按键.点击鼠标的情况.这是通过设置硬件的中断函数实现的,可以说硬件 ...

  5. 30天自制操作系统(三)进入32位模式并导入C语言

    1 制作真正的IPL IPL(Initial Program Loader),启动程序装载器,但是之前并没有实质性的装载任何程序,这次作者要开始装载程序了. 虽然现在开发的操作系统啥功能也没有,作者说 ...

  6. 自制操作系统(七) 加快中断处理,和加入FIFO缓冲区

    参考书籍<30天自制操作系统>.<自己动手写操作系统> 2016-05-26.2016-07-09 主要是加快中断处理,和加入FIFO缓冲区. 因为之前是将打印字符的代码放在了 ...

  7. 自制操作系统(二) 让bootsector开机启动打印一首诗

    qq:992591601 欢迎交流 2016-03-31作 2016-06-01.2016-06-27改 我总结了些基本原理: 1.软盘的第一个扇区为启动区 2.计算机读软盘是以512字节为单位来读写 ...

  8. 从你的u盘启动:30天自制操作系统第四天u盘启动学习笔记

    暑假学习小日本的那本书:30天自制操作系统 qq交流群:122358078    ,更多学习中的问题.资料,群里分享 developing environment:ubuntu 关于u盘启动自己做的操 ...

  9. 30天自制操作系统第九天学习笔记(u盘软盘双启动版本)

    暑假学习小日本的那本书:30天自制操作系统 qq交流群:122358078    ,更多学习中的问题.资料,群里分享 environment:开发环境:ubuntu 第九天的课程已学完,确实有点不想写 ...

随机推荐

  1. ansible核心模块playbook介绍

    ansible的playbook采用yaml语法,它简单地实现了json格式的事件描述.yaml之于json就像markdown之于html一样,极度简化了json的书写.在学习ansible pla ...

  2. Perl中神奇的@EXPORT

    @EXPORT Perl通过继承,可以使子类可以像使用本地方法一样使用其基类的方法. 一个类如果想把自己的方法(变量)暴露给别人使用(比如一些公共基础类的的通用方法或变量),还可将直接将方法(变量)添 ...

  3. 第二阶段:2.商业需求文档MRD:5.MRD-Roadmap及规划

    产品路线图可以用泳道图来实现.将之前做过的泳道图的角色换为阶段即可. 可以以月为单位.左边就是一些产品的功能. 基础功能,有的功能会跨月甚至夸功能模块.比如图中的会员等级. 通过线段来联系各个功能与先 ...

  4. JSR-133内存模型手册

    1.介绍 JVM支持多种线程的执行,Threads代表的是线程类,位于java.lang.Thread包下,唯一的方式就是为用户在这个类下的对象创建线程,每一个线程关联着一个对象,一个线程将在star ...

  5. 洛谷p1119--灾难后重建(Floyd不仅仅是板子)

    问题描述 询问次数  5 000 00,   顶点数  200 怎么办? dijkstra?对不起,超时了/. 时间限制是1秒,询问5 000 00 ,每次dijsktra要跑n*n*logm 次,稳 ...

  6. mysql主从之主机名导致主从机制失败的问题

    一 主库 mysql主服务器的正确配置需要指定log-bin.log-bin-index server-id = 1 log-bin=master-bin log-bin-index = master ...

  7. 前端——jQuery介绍

    目录 jQuery介绍 jQuery的优势 jQuery内容: jQuery版本 jQuery对象 jQuery基础语法 查找标签 基本选择器 层级选择器: 基本筛选器: 属性选择器: 表单筛选器: ...

  8. nginx部署vue跨域proxy方式

    server { listen 80; charset utf-8; #server_name localhost; server_name you_h5_name; ###VUE项目H5域名 err ...

  9. 【题解】SDOI2010所驼门王的宝藏(强连通分量+优化建图)

    [题解]SDOI2010所驼门王的宝藏(强连通分量+优化建图) 最开始我想写线段树优化建图的说,数据结构学傻了233 虽然矩阵很大,但是没什么用,真正有用的是那些关键点 考虑关键点的类型: 横走型 竖 ...

  10. $ CometOJ-Contest\#11\ D$ $Kruscal$重构树

    正解:$Kruscal$重构树 解题报告: 传送门$QwQ$ 发现一个图上搞就很麻烦,考虑变为生成树达到原有效果. 因为在询问的时候是要求走到的点编号尽量小,发现这个时候点的编号就成为限制了,于是不难 ...