loader.S 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423
  1. ; loader
  2. ; 位于硬盘第 2 扇区(LBA 地址)
  3. ; ----------------------------------------------------------------
  4. %include "boot.inc"
  5. SECTION LOADER vstart=LOADER_BASE_ADDR
  6. LOADER_STACK_TOP equ LOADER_BASE_ADDR
  7. ; 构建 GDT 及其内部描述符(GDT 的第 0 个描述符不可用)
  8. GDT_BASE: dd 0x00000000
  9. dd 0x00000000
  10. CODE_DESC: dd 0x0000FFFF
  11. dd DESC_CODE_HIGH4
  12. DATA_STACK_DESC: dd 0x0000FFFF
  13. dd DESC_DATA_HIGH4
  14. VIDEO_DESC: dd 0x80000007 ;limit=(0xbffff-0xb8000)/4k=0x7
  15. dd DESC_VIDEO_HIGH4 ;此时 dpl 为 0
  16. GDT_SIZE equ $ - GDT_BASE
  17. GDT_LIMIT equ GDT_SIZE - 1
  18. times 120 dd 0 ; 此处预留 60 个描述符的空位
  19. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  20. ; total_mem_bytes 用于保存内在容量,以字节为单位,此位置比较好记
  21. ; 当前偏移 loader.bin 文件头(4*8+60*8=512 字节)0x200 字节 loader.bin 的加载地址是 0x900
  22. ; 故 total_mem_bytes 内存地址是 0xb00
  23. ; offset 0x200
  24. total_mem_bytes dd 0
  25. ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;
  26. SELECTOR_CODE equ (0x0001<<3) + TI_GDT + RPL0 ; 相当于(CODE_DESC - GDT_BASE)/8 + TI_GDT + RPL0
  27. SELECTOR_DATA equ (0x0002<<3) + TI_GDT + RPL0
  28. SELECTOR_VIDEO equ (0x0003<<3) + TI_GDT + RPL0
  29. ; 以下是 gdt 的指针,前 2 个字节是 gdt 界限,后 4 字节是 gdt 起始地址
  30. gdt_ptr dw GDT_LIMIT
  31. dd GDT_BASE
  32. loadermsg db '2 loader in real.'
  33. ; 人工对齐 total_mem_bytes4+gdt_ptr6+loadermsg17+ards_buf227+ards_nr2, 共 256 字节
  34. ards_buf times 227 db 0
  35. ards_nr dw 0 ; 用于记录 ARDS 结构数量
  36. ;----------------------------------------------------------------
  37. ; INT 0x10 功能号:0x13 功能描述: 打印字符串
  38. ;----------------------------------------------------------------
  39. ; 输入:
  40. ; AH 子功能号 13H
  41. ; BH = 页码
  42. ; BL = 属性(若 AL=00H 或 01H)
  43. ; CX = 字符串长度
  44. ; (DH、DL) = 坐标(行,列)
  45. ; ES:BP = 字符串地址
  46. ; AL=显示输出方式
  47. ; 0 ---- 字符串中只含显示字符,其显示属性在 BL 中,显示后,光标位置不变
  48. ; 1 ---- 字符串中只含显示字符,其显示属性在 BL 中,显示后,光标位置不变
  49. ; 2 ---- 字符串中含显示字符和显示属性。显示后,光标位置不变
  50. ; 3 ---- 字符串中含显示字符和显示属性。显示后,光标位置改变
  51. ; 无返回值
  52. ; offset = 0x200 + 0x100 = 0x300
  53. loader_start:
  54. ;------------------------ 获取物理内存容量 ---------------------------------------
  55. ; int 15h eax=E820h, edx = 534D4150h('SMAP') 获取内存布局
  56. xor ebx, ebx ; 第一次调用时,ebx 值要为 0
  57. mov edx, 0x534d4150 ; edx 只赋值一次,循环体中不会改变(字符串 SMAP 的 ASCII 码)
  58. mov di, ards_buf ; ards 结构缓冲区 es:di(es mbr.S 中已经赋值)
  59. .e820_mem_get_loop:
  60. mov eax, 0xe820 ; 执行 int 0x15 后, eax 值变为 0x534d150, 所以每次执行 Int 前都要更新子功能号
  61. mov ecx, 20 ; ards 地址范围描述结构大小是 20 字节
  62. int 0x15
  63. jc .e820_failed_so_try_e801 ; cf 位为 1 则有错误发生,尝试 0xe801 子功能
  64. add di, cx ; 使 di 增加 20 字节指向缓冲区中新的 ARDS 结构位置
  65. inc word [ards_nr] ; 记录 ards 数量
  66. cmp ebx, 0 ; 若 ebx 为 0 且 cf 不为 1,这说明 ards 全部返回,当前已是最后一个
  67. jnz .e820_mem_get_loop
  68. ; 在所有 ards 结构中,找出(base_addr_low + length_low) 的最大值,即内存的容量
  69. mov ecx, [ards_nr] ; 遍历第一个 ards 结构体,循环次数是 ards 的数量
  70. mov ebx, ards_buf
  71. xor edx, edx ; edx 为最大的内存容量,在此先清 0
  72. .find_max_mem_area: ; 不需要判断 type 是否为 1, 最大的内存块一定是可被使用的
  73. mov eax, [ebx] ; base_addr_low
  74. add eax, [ebx+8] ; length_low
  75. add ebx, 20 ; 指向缓冲区中下一个 ards 结构
  76. cmp edx, eax
  77. jge .next_ards
  78. mov edx, eax ; edx 为总内存大小
  79. .next_ards:
  80. loop .find_max_mem_area
  81. jmp .mem_get_ok
  82. ; int 15h ax=E801h 获取内存大小,最大支持 4G
  83. ; 返回后, ax cx 值一样, 以 KB 为单位, bx dx 值一样, 以 64KB 为单位
  84. ; 在 ax 和 cx 寄存器中低 16MB, 在 bx 和 dx 寄存器中为 16MB 到 4GB
  85. .e820_failed_so_try_e801:
  86. mov ax, 0xe801
  87. int 0x15
  88. jc .e801_failed_so_try_88 ; 若当前 e801 方法失败,就尝试 0x88 方法
  89. ;;; 1 先算出低 15MB 的内存 ax 和 cx 中是以 KB 为单位的内存数量,将其转换为以 byte 为单位
  90. mov cx, 0x400 ; 0x400=1024, cx 和 ax 值一样,cx 用作乘数
  91. mul cx
  92. shl edx, 16
  93. and eax, 0xFFFF
  94. or edx, eax
  95. add edx, 0x100000 ; ax 只是 15MB,故要加 1MB=1024*1024=1048576=0x100000
  96. mov esi, edx ; 先把低 15MB 的内存容量存入 esi 寄存器备份
  97. ;;; 2 再将 16MB 以上的内存转换为 byte 为单位, 寄存器 bx 和 dx 中是以 64KB 为单位的内存数量
  98. xor eax, eax
  99. mov ax, bx
  100. mov ecx, 0x10000 ; 64*1024=0x10000
  101. mul ecx ; 32 位乘法,默认的被乘数是 eax,积为 64 位,高 32 位存入 edx, 低 32 位存入 eax
  102. add esi, eax ; 由于些方法只能测试 4G 以内的内存,故 32 位 eax 足够, edx 为 0
  103. mov edx, esi ; edx 为总内存大小
  104. jmp .mem_get_ok
  105. ; int 15h ah=88h 获取内存大小,只能获取 64MB 之内
  106. .e801_failed_so_try_88:
  107. ; int 15 后,ax 存入的是以 KB 为单位的内存容量
  108. mov ah, 0x88
  109. int 0x15
  110. jc .error_hlt
  111. and eax, 0xffff
  112. mov cx, 0x400 ; 1024
  113. mul cx
  114. ; 16 位乘法,被乘数是 ax,积为 32 位,高 16 位在 dx 中,低 16 位在 ax 中
  115. shl edx, 16 ; 把 dx 移动高 16 位
  116. and eax, 0xFFFF
  117. or edx, eax ; 把积的低 16 位组合到 edx 中,成为 32 位的积
  118. add edx, 0x100000 ; 0x88 子功能只会返回 1MB 以上的内存,故实际内存大小要加上 1MB=1024*1024=1048576=0x100000
  119. jmp .mem_get_ok
  120. .error_hlt:
  121. mov byte [gs:0], 'e'
  122. mov byte [gs:1], 'r'
  123. mov byte [gs:2], 'r'
  124. mov byte [gs:3], 'o'
  125. mov byte [gs:4], 'r'
  126. mov byte [gs:5], '_'
  127. mov byte [gs:6], 'h'
  128. mov byte [gs:7], 'l'
  129. mov byte [gs:8], 't'
  130. jmp $
  131. .mem_get_ok:
  132. mov [total_mem_bytes], edx ; 将内存换为 byte 单位后存入 total_mem_bytes 处
  133. ; 显示 loadermsg
  134. ; mov sp, LOADER_BASE_ADDR
  135. ; mov bp, loadermsg ; ES:BP = 字符串地址
  136. ; mov cx, 17 ; CX = 字符串长度
  137. ; mov ax, 0x1301 ; AH = 13h, AL = 01h
  138. ; mov bx ,0x001f ; 页号 0(BH=0)蓝底粉红色(BL=1fh)
  139. ; mov dx, 0x1800 ; 坐标(行,列)
  140. ; int 0x10 ; 10h 号中断
  141. ;---------------------- 准备进入保护模式 ------------------------------------------
  142. ; 1 打开 A20
  143. ; 2 加载 GDT
  144. ; 3 将 cr0 的 pe 位置 1
  145. ;-------------------------- 打开 A20 --------------------------------
  146. in al, 0x92
  147. or al, 0000_0010b
  148. out 0x92, al
  149. ;-------------------------- 加载 GDT --------------------------------
  150. lgdt [gdt_ptr]
  151. ;-------------------------- cr0 第 0 位置 1 --------------------------
  152. mov eax, cr0
  153. or eax, 0x00000001
  154. mov cr0, eax
  155. jmp dword SELECTOR_CODE:p_mode_start ; 刷新流水线
  156. [bits 32]
  157. p_mode_start:
  158. mov ax, SELECTOR_DATA
  159. mov ds, ax
  160. mov es, ax
  161. mov ss, ax
  162. mov esp, LOADER_STACK_TOP
  163. mov ax, SELECTOR_VIDEO
  164. mov gs, ax
  165. ; ------------------------ 加载内核 kernel ------------------------
  166. ;!! 加载到 0x70000~0x9fbff, 190KB 的字节空间,kernel 不超过 100KB
  167. mov eax, KERNEL_START_SECTOR ; kernel.bin 所在的扇区号
  168. mov ebx, KERNEL_BIN_BASE_ADDR
  169. mov ecx, 200 ; 读入的扇区数
  170. call rd_disk_m_32
  171. ; 创建页目录及页表并初始化内存位图
  172. call setup_page
  173. ; 将描述符表地址及偏移量写入内存 gdt_ptr, 一会用新地址重新加载
  174. sgdt [gdt_ptr]
  175. ; 将 gdt 描述符中视频段描述符中的段基地+0xc0000000
  176. mov ebx, [gdt_ptr + 2] ; gdt_base 地址
  177. ; 视频段是 3 个段描述符,第个描述符 8 字节, 故 0x18
  178. ; 段描述符的高 4 字节的第 31~24 位是段基址
  179. or dword [ebx + 0x18 + 4], 0xC0000000
  180. ; 将 gdt 的基址加上 0xC0000000 使其成人内核所在的高地址
  181. add dword [gdt_ptr + 2], 0xC0000000
  182. add esp, 0xC0000000 ; 将栈指针同样映射到内核地址
  183. ; 把页目录地址赋给 cr3
  184. mov eax, PAGE_DIR_TABLE_POS
  185. mov cr3, eax
  186. ; 打开 cr0 的 pg 位(第 31 位).
  187. mov eax, cr0
  188. or eax, 0x80000000
  189. mov cr0, eax
  190. ; 在开启分页后,用 gdt 新的地址重新加载
  191. lgdt [gdt_ptr]
  192. ; 初始化 kernel
  193. jmp SELECTOR_CODE:enter_kernel
  194. enter_kernel:
  195. call kernel_init
  196. mov esp, 0xc009f000
  197. jmp KERNEL_ENTRY_POINT
  198. jmp $
  199. ; -------------------------- 创建页目录及页表 ----------------------------------------
  200. ; 页目录项
  201. ; 31 12 11 9 8 7 6 5 4 3 2 1 0
  202. ; -----------------------------------------------------------------------------
  203. ; | 页表物理页地址 | AVL | G | 0 | D | A | PCD | PWT | US | RW | P |
  204. ; -----------------------------------------------------------------------------
  205. ;
  206. ; 页表项
  207. ; 31 12 11 9 8 7 6 5 4 3 2 1 0
  208. ; -----------------------------------------------------------------------------
  209. ; |物理页地址31-12 | AVL | G | PAT | D | A | PCD | PWT | US | RW | P |
  210. ; -----------------------------------------------------------------------------
  211. setup_page:
  212. ; 先把页目录占用的空间逐字节清 0
  213. mov ecx, 4096 ; 页目录大小 4KB = 4096B = 0x1000
  214. mov esi, 0
  215. .clear_page_dir:
  216. mov byte [PAGE_DIR_TABLE_POS + esi], 0
  217. inc esi
  218. loop .clear_page_dir
  219. ; 开始创建页目录项(PDE)
  220. .create_pde: ; 创建 Page Directory Entry Table
  221. mov eax, PAGE_DIR_TABLE_POS ; 0x100000
  222. add eax, 0x1000 ; 此时 eax 为第一个页表的位置及属性 0x100000 + 0x1000 = 0x101000
  223. mov ebx, eax ; 此处为 ebx 赋值,是为 .create_pte 做准备,ebx 为基址
  224. ; 下面将页目录 0 和 0xc00 都存为第一个页表的地址,第个页表 4MB 内存
  225. ; 这样 0xc03fffff 以下的地址和 0x003fffff 以下的地址都指向相同的页表
  226. ; 这是为将地址映射为内核地址做准备
  227. or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项的属性 RW 和 P 位为 1, US 为 1,表示用户属性,所有特权级别都可以访问
  228. mov [PAGE_DIR_TABLE_POS + 0x0], eax ; 页目录表中的第一个目录项写入第一个页表的位置(0x101000) 及属性 (7)
  229. mov [PAGE_DIR_TABLE_POS + 0xC00], eax ; 一个页目录项占 4 字节, 0xc00 表示第 768 个页表占用的目录项
  230. ;; 0x00000000 ~~ 0x3fffffff 是第一个 1 GB 内存
  231. ;; 0x40000000 ~~ 0x7fffffff 是第二个 1GB 内存
  232. ;; 0x80000000 ~~ 0xbfffffff 是第三个 1GB 内存
  233. ;; 0xc0000000 ~~ 0xffffffff 是第四个 1GB 内存
  234. ;; 0xc00 以上的目录项用于内核空间
  235. ;; 也就是页表的 0xc0000000 ~~ 0xffffffff 共计 1G 属于内核
  236. ;; 0x0 ~~ 0xbfffffff 共计 3G 属于用户进程
  237. sub eax, 0x1000
  238. mov [PAGE_DIR_TABLE_POS + 4092], eax ; 1024 * 4 - 4 = 4092 使最后一个目录项指向页目录表自己的地址
  239. ; 下面创建页表项(PTE) 256 个页表项
  240. ;; 第 0 页, 分配物理地址 0~0x3fffff 之间的物理页
  241. ;; 1M 低端内存中,虚拟地址等于物理地址
  242. mov ecx, 256 ; 1M 低端内存(1024K) / 每页大小 4K = 256
  243. mov esi, 0
  244. mov edx, PG_US_U | PG_RW_W | PG_P ; 属性 7, US=1, RW=1,P=1
  245. .create_pte: ; 创建 Page Table Entry
  246. mov [ebx+esi*4], edx ; 此时ebx=0x101000,第一个页表地址
  247. add edx, 4096
  248. inc esi
  249. loop .create_pte
  250. ; 创建内核其它页表的 PDE
  251. mov eax, PAGE_DIR_TABLE_POS
  252. add eax, 0x2000 ; 此时 eax 为第二个页表的位置
  253. or eax, PG_US_U | PG_RW_W | PG_P ; 页目录项属性都为 1
  254. mov ebx, PAGE_DIR_TABLE_POS
  255. mov ecx, 254 ; 范围为第 769 ~ 1022 的所有目录项数量
  256. mov esi, 769
  257. .create_kernel_pde:
  258. mov [ebx+esi*4], eax
  259. inc esi
  260. add eax, 0x1000
  261. loop .create_kernel_pde
  262. ret
  263. ; ----------------------------------------------------------------
  264. ; 读取硬盘数据
  265. ; EAX=LBA 地址
  266. ; EBX=将数据写入的内存地址
  267. ; ECX=读取的扇区数
  268. rd_disk_m_32:
  269. ; ----------------------------------------------------------------
  270. mov esi, eax ; 备份 eax
  271. mov edi, ecx ; 备份 ecx
  272. ; 1. 设置要读取的扇区数
  273. mov dx, 0x1f2
  274. mov al, cl
  275. out dx, al
  276. mov eax, esi ; 恢复 eax
  277. ; 2. 设置 LBA 地址,存入 0x1f3 - 0x1f6 端口中
  278. ; LBA 地址 7 ~ 0 位写入端口 0x1f3
  279. mov dx, 0x1f3
  280. out dx, al
  281. ; LBA 地址 15 ~ 8 位写入端口 0x1f4
  282. mov cl, 8
  283. shr eax, cl ; 右移 8 位
  284. mov dx, 0x1f4
  285. out dx, al
  286. ; LBA 地址 23 ~ 16 位写入端口 0x1f5
  287. shr eax, cl
  288. mov dx, 0x1f5
  289. out dx, al
  290. shr eax, cl
  291. and al, 0x0f ; 设置 LBA 地址 24~27 位
  292. or al, 0xe0 ; 设置 7~4 位为 1110, 表示 LBA 模式
  293. mov dx, 0x1f6 ; Device
  294. out dx, al
  295. ; 3. 向 0x1f7 端口写入命令 0x20
  296. mov dx, 0x1f7 ; Command
  297. mov al, 0x20
  298. out dx, al
  299. ; 4. 检测硬盘状态
  300. ; 使用同一端口0x1f7,写时表示写入命令字,读时表示读入硬盘状态
  301. .not_ready:
  302. nop ; 什么也不做,目的是为了减少对硬盘的打扰
  303. in al, dx ; 读入硬盘状态
  304. and al, 0x88 ; 第 4 位为 1 表示硬盘控制器已准备好数据传输,第 7 位为 1 表示硬盘忙
  305. cmp al, 0x08
  306. jnz .not_ready ; 硬盘没有准备数据传输,继续等待
  307. ; 5. 从 0x1f0 端口读取数据
  308. ; di 为要读取的扇区数,一个扇区 512 字节,每次读入一个字,共需要 di*512/2 次,所以 di*256
  309. mov ax, di
  310. mov dx, 256
  311. mul dx ; dx:ax = ax * 256
  312. mov cx, ax
  313. mov dx, 0x1f0
  314. .go_on_read:
  315. in ax, dx
  316. mov [ebx], ax
  317. add ebx, 2
  318. loop .go_on_read
  319. ret
  320. ; ------------------------ 将 kernel.bin 中的 segment 拷贝到编译的地址 ------------------------
  321. kernel_init:
  322. xor eax, eax
  323. xor ebx, ebx ; ebx 记录程序头表地址
  324. xor ecx, ecx ; cx 记录程序头表的 program header 数量
  325. xor edx, edx ; dx 记录 program header 尺寸,即 e_phentsize
  326. mov dx, [KERNEL_BIN_BASE_ADDR + 42] ; 偏移文件 42 字节处的属性是 e_phentsize,表示 program header 大小
  327. mov ebx, [KERNEL_BIN_BASE_ADDR + 28] ; 偏移文件 28 字节的地方是 e_phoff, 表示第 1 个 program header 在文件中的偏移量
  328. add ebx, KERNEL_BIN_BASE_ADDR
  329. mov cx, [KERNEL_BIN_BASE_ADDR + 44] ; 偏移文件开始部分 44 字节的地方是 e_phnum, 表示有几个 program header
  330. .each_segment:
  331. cmp byte [ebx], PT_NULL ; 若 p_type 等于 PT_NULL,说明此 program header 未使用
  332. je .PTNULL
  333. ; 为函数 memycpy 压入参数, 参数是从右到左依然压入
  334. ; 函数原型 memycpy(dst, src, size)
  335. push dword [ebx + 16] ; program header 中偏移 16 字节的地方是 p_filesz 第三个参数: size
  336. mov eax, [ebx + 4] ; 距程序头偏移量为 4 字节的位置是 p_offset
  337. add eax, KERNEL_BIN_BASE_ADDR ; 加上 kernel.bin 被加载的物理地址, eax 为该段的物理地址
  338. push eax ; 第二个参数: 源地址 src
  339. push dword [ebx + 8] ; 偏移程序头 8 字节位置是p_vaddr, 目的地址
  340. call mem_cpy ; 调用 mem_cpy 完成段自制
  341. add esp, 12 ; 清理栈中压入的三个参数
  342. .PTNULL
  343. add ebx, edx ; edx 为 program header 大小,此时 ebx 指向下一个 program header
  344. loop .each_segment
  345. ret
  346. ; -------------------------------- 逐字节拷贝 mem_cpy(dst, src, size) --------------------------------
  347. ; 输入:栈中三个参数 (dst, src, size)
  348. ; 输出: 无
  349. ; --------------------------------------------------------------------------------------------------
  350. mem_cpy:
  351. cld ; mem_cpy 使 DF (Direction Flag) 复位 DF = 0
  352. push ebp
  353. mov ebp, esp
  354. push ecx ; rep 指令用到了 ecx, 故先备份
  355. mov edi, [ebp + 8] ; dst ES:DI
  356. mov esi, [ebp + 12] ; src DS:SI
  357. mov ecx, [ebp + 16] ; size
  358. rep movsb ; 逐字节拷贝
  359. ; 恢复环境
  360. pop ecx
  361. pop ebp
  362. ret