操作系统启动的第一条指令?

我们以 X86作为例子:

我们的电脑启动时需要先加载操作系统,那么操作系统如何加载?如何知道操作系统源代码位于何处?

首先在操作系统中如何找到源码地址,那么应该时依靠指针 IP 地址,那么 IP =?

  1. X86 PC 在刚开机时 CPU 处于 实模式 (PS: 和保护模式对应,实模式的寻址 CS:IP CS 左移4伪+IP和保护模式不一样)
  2. 开机时, CS=0xFFFF IP=0x0000 (PS: 自动设置)
  3. 寻址 0xFFFF (ROM BIOS 映射区)
  4. 检查 RAM 键盘 显示器 软硬磁盘
  5. 将磁盘 0 磁道 0 扇区 读入 0x7c00 处 (PS: 0 磁道 0 扇区操作系统的引导扇区)
  6. 设置 CS=0x7c0 IP=0x0000

操作系统启动 - 图1

BIOS —> bootsect.s —> stetup.s —> system OS

引导扇区代码 : bootsect.s

  1. !
  2. ! SYS_SIZE is the number of clicks (16 bytes) to be loaded.
  3. ! 0x3000 is 0x30000 bytes = 196kB, more than enough for current
  4. ! versions of linux
  5. !
  6. SYSSIZE = 0x3000
  7. !
  8. ! bootsect.s (C) 1991 Linus Torvalds
  9. !
  10. ! bootsect.s is loaded at 0x7c00 by the bios-startup routines, and moves
  11. ! iself out of the way to address 0x90000, and jumps there.
  12. !
  13. ! It then loads 'setup' directly after itself (0x90200), and the system
  14. ! at 0x10000, using BIOS interrupts.
  15. !
  16. ! NOTE! currently system is at most 8*65536 bytes long. This should be no
  17. ! problem, even in the future. I want to keep it simple. This 512 kB
  18. ! kernel size should be enough, especially as this doesn't contain the
  19. ! buffer cache as in minix
  20. !
  21. ! The loader has been made as simple as possible, and continuos
  22. ! read errors will result in a unbreakable loop. Reboot by hand. It
  23. ! loads pretty fast by getting whole sectors at a time whenever possible.
  24. .globl begtext, begdata, begbss, endtext, enddata, endbss
  25. .text
  26. begtext:
  27. .data
  28. begdata:
  29. .bss
  30. begbss:
  31. .text
  32. SETUPLEN = 4 ! nr of setup-sectors
  33. BOOTSEG = 0x07c0 ! original address of boot-sector
  34. INITSEG = 0x9000 ! we move boot here - out of the way
  35. SETUPSEG = 0x9020 ! setup starts here
  36. SYSSEG = 0x1000 ! system loaded at 0x10000 (65536).
  37. ENDSEG = SYSSEG + SYSSIZE ! where to stop loading
  38. ! ROOT_DEV: 0x000 - same type of floppy as boot.
  39. ! 0x301 - first partition on first drive etc
  40. ROOT_DEV = 0x306
  41. entry start
  42. start:
  43. mov ax,#BOOTSEG
  44. mov ds,ax
  45. mov ax,#INITSEG
  46. mov es,ax
  47. mov cx,#256
  48. sub si,si
  49. sub di,di
  50. rep
  51. movw
  52. jmpi go,INITSEG
  53. go: mov ax,cs
  54. mov ds,ax
  55. mov es,ax
  56. ! put stack at 0x9ff00.
  57. mov ss,ax
  58. mov sp,#0xFF00 ! arbitrary value >>512
  59. ! load the setup-sectors directly after the bootblock.
  60. ! Note that 'es' is already set up.
  61. load_setup:
  62. mov dx,#0x0000 ! drive 0, head 0
  63. mov cx,#0x0002 ! sector 2, track 0
  64. mov bx,#0x0200 ! address = 512, in INITSEG
  65. mov ax,#0x0200+SETUPLEN ! service 2, nr of sectors
  66. int 0x13 ! read it
  67. jnc ok_load_setup ! ok - continue
  68. mov dx,#0x0000
  69. mov ax,#0x0000 ! reset the diskette
  70. int 0x13
  71. j load_setup
  72. ok_load_setup:
  73. ! Get disk drive parameters, specifically nr of sectors/track
  74. mov dl,#0x00
  75. mov ax,#0x0800 ! AH=8 is get drive parameters
  76. int 0x13
  77. mov ch,#0x00
  78. seg cs
  79. mov sectors,cx
  80. mov ax,#INITSEG
  81. mov es,ax
  82. ! Print some inane message
  83. mov ah,#0x03 ! read cursor pos
  84. xor bh,bh
  85. int 0x10
  86. mov cx,#24
  87. mov bx,#0x0007 ! page 0, attribute 7 (normal)
  88. mov bp,#msg1
  89. mov ax,#0x1301 ! write string, move cursor
  90. int 0x10
  91. ! ok, we've written the message, now
  92. ! we want to load the system (at 0x10000)
  93. mov ax,#SYSSEG
  94. mov es,ax ! segment of 0x010000
  95. call read_it
  96. call kill_motor
  97. ! After that we check which root-device to use. If the device is
  98. ! defined (!= 0), nothing is done and the given device is used.
  99. ! Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
  100. ! on the number of sectors that the BIOS reports currently.
  101. seg cs
  102. mov ax,root_dev
  103. cmp ax,#0
  104. jne root_defined
  105. seg cs
  106. mov bx,sectors
  107. mov ax,#0x0208 ! /dev/ps0 - 1.2Mb
  108. cmp bx,#15
  109. je root_defined
  110. mov ax,#0x021c ! /dev/PS0 - 1.44Mb
  111. cmp bx,#18
  112. je root_defined
  113. undef_root:
  114. jmp undef_root
  115. root_defined:
  116. seg cs
  117. mov root_dev,ax
  118. ! after that (everyting loaded), we jump to
  119. ! the setup-routine loaded directly after
  120. ! the bootblock:
  121. jmpi 0,SETUPSEG
  122. ! This routine loads the system at address 0x10000, making sure
  123. ! no 64kB boundaries are crossed. We try to load it as fast as
  124. ! possible, loading whole tracks whenever we can.
  125. !
  126. ! in: es - starting address segment (normally 0x1000)
  127. !
  128. sread: .word 1+SETUPLEN ! sectors read of current track
  129. head: .word 0 ! current head
  130. track: .word 0 ! current track
  131. read_it:
  132. mov ax,es
  133. test ax,#0x0fff
  134. die: jne die ! es must be at 64kB boundary
  135. xor bx,bx ! bx is starting address within segment
  136. rp_read:
  137. mov ax,es
  138. cmp ax,#ENDSEG ! have we loaded all yet?
  139. jb ok1_read
  140. ret
  141. ok1_read:
  142. seg cs
  143. mov ax,sectors
  144. sub ax,sread
  145. mov cx,ax
  146. shl cx,#9
  147. add cx,bx
  148. jnc ok2_read
  149. je ok2_read
  150. xor ax,ax
  151. sub ax,bx
  152. shr ax,#9
  153. ok2_read:
  154. call read_track
  155. mov cx,ax
  156. add ax,sread
  157. seg cs
  158. cmp ax,sectors
  159. jne ok3_read
  160. mov ax,#1
  161. sub ax,head
  162. jne ok4_read
  163. inc track
  164. ok4_read:
  165. mov head,ax
  166. xor ax,ax
  167. ok3_read:
  168. mov sread,ax
  169. shl cx,#9
  170. add bx,cx
  171. jnc rp_read
  172. mov ax,es
  173. add ax,#0x1000
  174. mov es,ax
  175. xor bx,bx
  176. jmp rp_read
  177. read_track:
  178. push ax
  179. push bx
  180. push cx
  181. push dx
  182. mov dx,track
  183. mov cx,sread
  184. inc cx
  185. mov ch,dl
  186. mov dx,head
  187. mov dh,dl
  188. mov dl,#0
  189. and dx,#0x0100
  190. mov ah,#2
  191. int 0x13
  192. jc bad_rt
  193. pop dx
  194. pop cx
  195. pop bx
  196. pop ax
  197. ret
  198. bad_rt: mov ax,#0
  199. mov dx,#0
  200. int 0x13
  201. pop dx
  202. pop cx
  203. pop bx
  204. pop ax
  205. jmp read_track
  206. /*
  207. * This procedure turns off the floppy drive motor, so
  208. * that we enter the kernel in a known state, and
  209. * don't have to worry about it later.
  210. */
  211. kill_motor:
  212. push dx
  213. mov dx,#0x3f2
  214. mov al,#0
  215. outb
  216. pop dx
  217. ret
  218. sectors:
  219. .word 0
  220. msg1:
  221. .byte 13,10
  222. .ascii "Loading system ..."
  223. .byte 13,10,13,10
  224. .org 508
  225. root_dev:
  226. .word ROOT_DEV
  227. boot_flag:
  228. .word 0xAA55
  229. .text
  230. endtext:
  231. .data
  232. enddata:
  233. .bss
  234. endbss:

我们对其中主要代码分析

  1. .globl begtext, begdata, begbss, endtext, enddata, endbss
  2. .text // 文本段
  3. begtext:
  4. .data // 数据段
  5. begdata:
  6. .bss // 未初始化段
  7. begbss:
  8. .text
  9. SETUPLEN = 4 // 设置扇区数
  10. BOOTSEG = 0x07c0 // boot-sector 的段地址,实模式下,左移4位为0x07c00
  11. INITSEG = 0x9000 // 我们把bootsect移动到这里
  12. SETUPSEG = 0x9020 // 开始加载
  13. SYSSEG = 0x1000 // 系统加载 0x10000 (65536).
  14. ENDSEG = SYSSEG + SYSSIZE // 停止加载的地址
  15. entry start // 程序入口
  16. start:
  17. mov ax,#BOOTSEG
  18. mov ds,ax // ds = 0x07c0
  19. mov ax,#INITSEG
  20. mov es,ax // es = 0x9000
  21. mov cx,#256 // cx = 256
  22. sub si,si // si = 0
  23. sub di,di // di = 0
  24. rep // 重复 后面的指令,是个前缀, 根据 ecx 判断重复次数即: 重复 movw 指令256 次
  25. movw // mov 指令是移动的意思, movw 是移动 2 字,最终效果是移动 512 字节 (注解1)
  26. jmpi go,INITSEG // 跳转指令 cs = INITSEG = 0x9000
  • 注解1 : movw 执行后会将 0x07c0:0x0000 处的 256 个字移动到 0x9000:0x0000 (PS : 这个的 256 个字 (512 byte) 恰好就是引导扇区的大小)
  • 那么问题来了,我们为什么要移动 bootsect.s 代码?
  1. go:
  2. mov ax,cs
  3. mov ds,ax
  4. mov es,ax // es = 0x9000
  5. mov ss,ax
  6. mov sp,#0xFF00
  7. load_setup: // 载入 setup 模块
  8. mov dx,#0x0000
  9. mov cx,#0x0002 // cl = 2
  10. mov bx,#0x0200 // bx = 0x0200 (对应十进制 512)
  11. mov ax,#0x0200+SETUPLEN // al = 4 (读取四个扇区)
  12. int 0x13 // (注解1)
  13. jnc ok_load_setup ! ok - continue
  14. mov dx,#0x0000
  15. mov ax,#0x0000 // 复位
  16. int 0x13
  17. j load_setup // 重读
  • 注解1 : 0x13 是 BIOS 读磁盘扇区的中断: ah=0x02, al=扇区数量 (SETUPLEN = 4 ) ,
    ch=柱面号 cl=开始扇区 dh=磁头号 dl=驱动器号 es:bx=内存地址
  • 为什么从第二个扇区开始读? 因为 boot 扇区占据了第一个扇区
  • 读取到哪里呢?
    读取到 es:bx 的位置, 即 0x9000:0x0200 , 因为 boot 扇区的代码是在 0x9000 处的并且大小是 512 字节,所以我们应该将 setup 扇区的代码放置于 boot 扇区代码之上

操作系统启动 - 图2

内存示意图:

操作系统启动 - 图3

读入 setup 模块后,就需要显示开机画满并读 system模块

  1. ok_load_setup: // 载入 setup 模块
  2. mov dl,#0x00
  3. mov ax,#0x0800 // ah = 8
  4. int 0x13
  5. mov ch,#0x00
  6. seg cs
  7. mov sectors,cx
  8. mov ax,#INITSEG
  9. mov es,ax
  10. mov ah,#0x03
  11. xor bh,bh
  12. int 0x10 // ah 功能号为3,表示读取光标位置
  13. mov cx,#24
  14. mov bx,#0x0007 // 7 是显示属性
  15. mov bp,#msg1 // 要显示的内容在内存中的位置(见下)
  16. mov ax,#0x1301 ! write string, move cursor
  17. int 0x10 // 显示字符
  18. mov ax,#SYSSEG
  19. mov es,ax ! segment of 0x010000
  20. call read_it // 读入 system 模块
  21. // 中间省略内容
  22. jmpi 0,SETUPSEG // 转入 0x9020:0x0000 执行 setup.s
  1. msg1:
  2. .byte 13,10
  3. .ascii "Loading system ..." // 要显示的内容,开机 logo
  4. .byte 13,10,13,10

setup.s

setup 将完成 OS 启动前的配置

  1. INITSEG = 0x9000 ! bootsect.s 的段地址
  2. SYSSEG = 0x1000 ! system loaded at 0x10000
  3. SETUPSEG = 0x9020 ! 本程序的段地址
  4. 0x90000 光标位置
  5. 0x90002 扩展内存数
  6. 0x9000c 显卡参数
  7. 0x901FC 根设备号
  1. start:
  2. mov ax,#INITSEG // INITSEG 在setup.s里的第17行 定义为0x9000
  3. mov ds,ax // ds = 0x9000
  4. mov ah,#0x03
  5. xor bh,bh
  6. int 0x10 // 10号中断的3号功能 读光标位置
  7. mov [0],dx // 取出光标位置 放到0x9000处
  8. // 获取物理内存的大小
  9. mov ah,#0x88
  10. int 0x15 // 中断
  11. mov [2],ax // 获取扩展内存大小,并放到0x9000处 (注解1)
  12. // 还会有读取显卡参数 设备号操作
  13. cli ; 不允许中断
  14. mov ax,#0x0000
  15. cld
  16. do_move:
  17. mov es,ax // es = 0
  18. add ax,#0x1000
  19. cmp ax,#0x9000
  20. jz end_move
  21. mov ds,ax // ds = 1000
  22. sub di,di
  23. sub si,si
  24. mov cx,#0x8000 // cx = 32768
  25. rep // 重复指令,将system模块挪到0地址处!(注解2)
  26. movsw
  27. jmp do_move
  • 注解1: 早期计算机中,地址总线只有20位,因此只能寻址1M以内的内存;而如今的计算机,都是8G,16G内存起步的,那么通常把1M以后的这些内存就叫扩展内存
  • 为什么要读取内存大小: 操作系统,就是帮我们管理硬件的,而内存就是一个重要的硬件。要管理好内存,首先得知道内存的多大。
  • 注解2 : 将 1000:0000 处代码复制到 0000:0000 处, 然后在 bootsect.s 中 1000:0000 处的代码就是 system 模块,因此这条指令的作用就是将 system 模块移动到 0 地址处
  • 那么我们就可以回到之前的一个问题:为什么bootsect.s 会将自己从 0x7c00处挪到9000处?因为要给system模块腾出空间。system模块很长,会覆盖到0x7c00处,如果正在执行的代码被覆盖了,肯定是不行的。同时,当时的system模块不会太大,不会覆盖到0x90000处的bootsect和setup模块 (当时system模块不会超过0x80000字节(即512kb),Linux0.11内核只有14000行左右,大概325KB大小)。操作系统诞生之初,功能还比较简单,代码也不会太多。
之后,system模块就会一直在0地址处。而system之后的内存,我们就可以用于运行我们自己的程序了,例如浏览器,Word等。 现在我们的操作系统已经加载到内存中, 现在我们就需要执行我们的操作系统代码,这一步称为: 进入保护模式, 将我们的控制权交给操作系统 在上面我们提高我们要进入保护模式,那么什么叫保护模式,我们又为什么要进入保护模式? > 早期计算机只支持1M的内存,这指的是早期的寻址方式,只支持1M。早期使用的是段基址 + 偏移地址这样的方式寻址的,这种方式不能满足 4G内存的寻址,因此,我们要切换到一个新的寻址模式。因此CPU接下来会从16位寻址模式(也叫实模式)切换到32位寻址模式(也叫保护模式)。 > 那么CPU是怎么切换寻址模式呢?根据一个寄存器:CR0 。 如果这个寄存器的最后一位是0,CPU就会用16位模式;如果是1,就用保护模式(其实就是换一条电路去寻址)。
  1. mov ax,#0x0001
  2. lmsw ax // 将ax的值赋给CR0寄存器,然后接下来的指令,就是用32位寻址模式了。
  3. jmpi 0,8 // 跳转到 0 地址处(原因见下) ip=0 cs=8
那么 32 寻址模式如何寻址? 这就要提到一个非常著名概念叫 GDT(全局描述表Global Descriptor Table),GDT表里面存放的才是基址。 当然这也是硬件帮我们实现的寻址方式(因为硬件快)。如何用GDT寻址? 在16位模式下,代码寻址是用CS:IP 实现的,而在32位模式下,CS不再左移4位产生一个地址,而是用作选择子,换句话说就是CS的内容是GDT表的下标,对应的GDT表项的内容,才是段基址。 因此,32位寻址模式是这样工作的:首先根据CS取出GDT表的内容作为基址,IP还是作为偏移地址,因此来产生一个新的地址,示意图:

操作系统启动 - 图4

同样的,保护模式下,中断例程的寻址方式也发生了变化:仿照GDT表,新建了一个IDT表(中断描述符表Interrupt Descriptor Table),int n 就用n进行查表取出中断例程的地址,然后执行:

操作系统启动 - 图5

那么问题又出现了, GDT 表内容是什么?

在 setup 中也定义了 GDT 表以及 IDT 表:

  1. idt_48: // 保护模式中断函数表
  2. .word 0 ! idt limit=0
  3. .word 0,0 ! idt base=0L
  4. gdt_48:
  5. .word 0x800 ! gdt limit=2048, 256 GDT entries
  6. .word 512+gdt,0x9 ! gdt base = 0X9xxxx
  7. gdt: .word 0,0,0,0
  8. .word 0x07FF 0x0000 0x9A00 0x00C0
  9. .word 0x07FF 0x0000 0x9200 0x00C0
我们可以看到有很多 word 指令,一个word就是16位,而GDT表一个表项占8字节(64位),因此每4个word就是 一个GDT的表项(其中,第一个表项为空不使用) 每个表项的组成如下:

操作系统启动 - 图6

而GTD的下标如何确定呢?依次为0,8,16………… 我们以setup.s 的表为例:

操作系统启动 - 图7

现在我们回到问题处 **jmpi 0,8**为什么跳转到 0 地址处?

由于 cs=8 所以我们查看这个对应小标内容:

  1. .word 0x07FF 0x0000 0x9A00 0x00C0
而这几个word是如何存放到GDT表的呢?
  1. .word 0x07FF 0x0000 0x9A00 0x00C0
  2. 在内存中,从高地址到 低地址极速 0x00C0 9A00 0000 07FF
  3. 用二进制展开来就是
  4. 0x00C0: 0000 0000 1100 0000
  5. 0x9A00: 1001 1010 0000 0000
  6. 0x0000: 0000 0000 0000 0000
  7. 0x07FF: 0000 0111 1111 1111

操作系统启动 - 图8

放完后,我们可以看到,段基址就是0。因此,jmpi 0,8其实就是跳到0地址处去执行。至此,set up的工作到此就完成。

:::success

  1. 因为操作系统就是管理硬件的,因此首先得知道硬件的情况:读了一些硬件参数并存到内存里
  2. 把system挪到0地址处,将来操作系统运行的时候,system模块会一直存在那里
  3. 然后启动了保护模式(通过修改CR0寄存器),最后运用应用了32位的汇编指令JMPI 0, 8 跳到了0地址处去执行
  4. 0地址就是system模块,因此后面就是操作系统运行起来了

:::

System 模块

:::success System 模块的第一部分代码是: head.s

:::

system 由许多文件编译而成,为什么是 head.s ? (Linux/Makefile)

操作系统启动 - 图9

我们通常会将操作系统编译后的样子称为 ImageImage读入内存后, 就会启动操作系统.

那么 head.s 做了什么?

1. 初始化 GDT IDT

之前的setup.s 里建立的GDT只是临时用于跳转而已,现在操作系统是真正的开始工作了,所以还要再次建立这个表 (PS 这里出现了 32 位汇编)
  1. startup_32:
  2. movl $0x10,%eax
  3. mov %ax,%ds
  4. mov %ax,%es
  5. mov %ax,%fs
  6. mov %ax,%gs ; 指向gdt0x10项(数据段)
  7. lss _stack_start,%esp ; 设置系统栈
  8. call setup_idt ; 初始化 IDT
  9. call setup_gdt ; 初始化 GDT

:::success 为什么 head.s 汇编和前面不一样,在这里使用了三种汇编 ?

  1. as86 汇编: 产生 16 位代码的 Intel 8086 汇编

:::

  1. mov ds, ax, ; ax ds, 目标操作数在前

:::success

  1. GNU as 汇编: 产生 32 位代码,使用 AT&T 系统 V语法

:::

  1. movl var, %eax ; (var) eax
  2. movb -4(%ebp), %al ; 取出一字节

:::success

  1. 内嵌汇编: gcc 编译 x.c 产生中间结果汇编文件 x.s

:::

  1. __asm__(“汇编语句”
  2. : 输出
  3. : 输入
  4. : 破坏部分描述);
  5. //例如
  6. __asm__(“movb
  7. %%fs:%2, %%al //%2表示addr,
  8. :”=a”(_res) //a表示使用eax,并编号%0
  9. :”0”(seg),”m”(*(addr)) //0或空表示使用与相应输出一样的寄存器 m表示使用内存
  10. );

2. main.c

当head.s 执行完后,接下来就是执行 main.c 代码了。
  1. after_page_tables:
  2. pushl $0 // 往栈中压入数据
  3. pushl $0
  4. pushl $0
  5. pushl $L6 ; return address for main, if it decides to.
  6. pushl $_main // 在栈中压入 main 函数, main 函数是 C 代码入口处
  7. jmp setup_paging
  8. L6: jmp L6 ; main应该永远不会回到这里,但以防万一,我们知道会发生什么。
  9. setup_paging:
  10. ; …………这里省略一些设置页表代码……
  11. ret
如何从汇编 跳去执行 C语言的main函数呢?怎么做到的? 我们知道,汇编执行子程序的话,可以通过跳转指令; C语言执行函数(子程序)的话,用的是调用函数的语句,例如调用方法b(假设需要传参),就用 b(int a, int b) 即可。但其实, C语言最后还是会翻译成汇编,然后才被CPU执行。而传参,可以靠栈来实现。 因此,本段汇编代码的一开始几条压栈语句,就是传参给CPU;然后将main函数的地址压到栈中;当setup_paging执行ret后,就回执行函数main了!(执行ret指令后,会将栈里的内容取出作为下一个要执行的代码的地址)

那么我们现在提出一个疑问 main 函数返回怎么样?

  1. 如果写过 c 语言 main 函数返回就意味着程序结束对应操作系统就是死机
  2. 如果意外返回呢? 我们看下面一段代码 L6: jmp L6这就形成了死循环

3. main.c

进行一些初始化

  1. void main(void)
  2. {
  3. // ........省略部分代码 ........
  4. mem_init(main_memory_start,memory_end);
  5. trap_init();
  6. blk_dev_init();
  7. chr_dev_init();
  8. tty_init();
  9. time_init();
  10. sched_init();
  11. buffer_init(buffer_memory_end);
  12. hd_init();
  13. floppy_init();
  14. sti();
  15. move_to_user_mode();
  16. if (!fork()) {
  17. init();
  18. }
  19. // ........省略部分代码 ........
  20. }
首先,为什么main函数的参数是void? 其实三个参数分别是envp,argv,argc,但目前版本的main没有使用,且我们在head.s 里可以看到,在push main函数之前,都压栈了3个0,所以这里是没有问题的 其次,我们可以看到,有很多的函数,并且都是带init字眼的,这些就是初始化内存,中断,时钟,硬盘,显示器等(Linux0.11不支持鼠标) 每一个都可以说很久很久,我们这里简单说说 mem_init(),其他的都类似

mem_init()

mem_init()顾名思义就是初始化内存的。前面我们提到操作系统就是管理硬件的,内存是一个重要的硬件,因此本函数就是初始化一些数据结构用来保存内存的信息,例如哪些被使用了,哪些是空闲的。

  1. #define USED 100
  2. #define PAGING_PAGES (PAGING_MEMORY>>12)
  3. static unsigned char mem_map [ PAGING_PAGES ] = {0,};
  4. void mem_init(long start_mem, long end_mem)
  5. {
  6. int i;
  7. HIGH_MEMORY = end_mem;
  8. for (i=0 ; i<PAGING_PAGES ; i++)
  9. mem_map[i] = USED;
  10. i = MAP_NR(start_mem);
  11. end_mem -= start_mem;
  12. end_mem >>= 12;
  13. while (end_mem-->0)
  14. mem_map[i++]=0;
  15. }
我们先看看参数,start_mem, end_mem是main函数里传参的,而main函数是这样调用的:
  1. static long main_memory_start = 0;
  2. static long memory_end = 0;
  3. //..........省略部分代码
  4. memory_end = (1<<20) + (EXT_MEM_K<<10);
  5. memory_end &= 0xfffff000;
  6. //..........省略部分代码
  7. mem_init(main_memory_start,memory_end);
其实内存的大小,在setup.s 里就已经存储到了 0x90000处,通过main函数里读取,然后传个 mem_init函数。 接下来<font style="color:rgb(0, 50, 60);"> </font>mem_init里做什么呢?首先有一个全局变量 <font style="color:rgb(0, 50, 60);">mem_map</font>,里面的值如果是0,表明内存没有被使用,如果是100,表明是已经被使用了。 所以在<font style="color:rgb(0, 50, 60);"> </font>mem_init首先将 0地址处(也就是自己system模块)的代码标记为已使用;然后将剩余的内存表明为未使用

操作系统启动 - 图10

来看看是怎么标记的。第14行的代码,用了右移运算符,右移12位其实就是除以2的12次方,也就是除以4k(在内存里我们是用页来管理的,后续会详细说)。 这样的话大家可以看到所谓内存初始化就是形成这样一个表格,用这个表格来表示内存中哪些地方是使用的,哪些地方是没使用的,所以后面是没使用的,而前面是使用的。

总结

1. make file

操作系统在磁盘里的逻辑示意图:

操作系统启动 - 图11第一个部分存放bootsect.s,第二个部分存放setup模块,第三个部分存放system模块。这个顺序是不能变的,如果有一点点差错,都会死机。那么,操作系统是如何从源代码,编译成我们想要的样子呢?这就得提到make file。

我们平时写C语言的时候,都是由IDE帮我们编译并运行的,不用关心程序运行在内存的哪里;而如果做操作系统,一切的事情都要自己控制。除了要写源码之外,还要确定如何编译操作系统生成镜像(这里镜像可以简单理解为操作系统安装包,英文名Image),也就是make file 初步讲下如何确保磁盘里的第一段代码是bootsect,第二段是setup呢?通过makefile,并且有很多依赖文件,例如head.s,main.s,驱动等等。把这些汇编成.o文件,然后链接起来生成system。 我们可以看到Linux根目录下有个 Makefile文件,我们可以看看里面的关键内容:
  1. Image: boot/bootsect boot/setup tools/system tools/build
  2. tools/build boot/bootsect boot/setup tools/system $(ROOT_DEV) > Image
最后操作系统镜像Image,是依赖于bootsect的,还依赖于setup,system,还有很多工具类(tools),最后将这些代码链接起来,生成镜像,然后就可以运行这个镜像了。

小结

  1. bootsect将操作系统从磁盘读进来,
  2. 而setup获得了一些参数,启动了保护模式,
  3. head初始化了GDT和IDT表,初始化了一些页表,然后跳到mian
  4. mian里又是很多int初始化函数,比如初始化内存,中断,时钟,硬盘,显示器

操作系统启动 - 图12

关于在启动过程中几个程序或模块在内存中的动态位置:

操作系统启动 - 图13