• 抄写Linux源码(Day12:从 MBR 到 C main 函数 (1) )


    回忆我们需要做的事情:
    为了支持 shell 程序的执行,我们需要提供:
    1.缺页中断(不理解为什么要这个东西,只是闪客说需要,后边再说)
    2.硬盘驱动、文件系统 (shell程序一开始是存放在磁盘里的,所以需要这两个东西)
    3.fork,execve, wait 这三个系统调用,也可以说是 进程调度 (否则无法 halt shell 程序并且启动另外的程序)
    4.键盘驱动、VGA/console/uart 驱动、中断处理 (支持键盘输入和屏幕显示)
    5.内存管理 (shell 启动其它进程时,不能共用内存,而是切换其它进程的页表)
    6.为了写代码方便,我们需要从 MBR 进入到 main 函数,这也是从 汇编 切换到 C 语言
    7.应用程序申请内存的接口

    经历了 Day11,看到了 Linux 0.11 中的内存管理代码后。

    显然,这样复杂的功能用汇编写会很花时间,所以我们得想办法从汇编转为 C 代码,随后进入一个 C main 函数

    直接看闪客文章第十回:https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247499838&idx=1&sn=6e7335be30fb24b03c54ccaaf135f236&chksm=c2c5ba93f5b23385a21191ea0ba60431340b195a3d63ae4d0e737f8274920d4c372940e4094c&scene=178&cur_album_id=2123743679373688834#rd

    在这里插入图片描述
    从上图可以看到,进入 C main 之间还要做许多工作,代码为 bootsect.s, setup.s 以及 head.s

    为什么要做这么多工作?为什么不能直接 jmp main 呢?

    首先 MBR 不能直接 jmp main。因为 MBR 的程序并不是一个和 C 程序链接在一起的程序,也不能是一个和 C 程序链接在一起的程序。因为 MBR 的大小仅为 512 字节,根本不可能和 C 程序链接在一起。

    所以 MBR 能做的事情只有:把内核(和 C main 链接的程序) 从磁盘上加载到内存中。

    那么 MBR 已经能把内核加载到内存里了,为什么还需要 setup.s 和 head.s 呢?

    这个问题后边再思考吧,我们先看看 bootsect.s 是如何把 setup.s 和 内核 从磁盘加载到内存上的

    继续看闪客文章第三回:https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247499307&idx=1&sn=c94575dde2b9bbdbabe2d7a832aa9ff4&chksm=c2c58486f5b20d907e314fdf88c5c25b8233b44f5a6a2864c418489e7ae993574150e92c9927&scene=21#wechat_redirect

    这一节就只讲了一小段汇编代码,我都做好注释了,如下:

    # .equ INITSEG, 0x9000		# cs 的值是 0x9000,但是会被左移四位,所以是跳转到 0x90000 + go
    # 执行了 ljmp 指令之后,cs:ip 的值会被修改,cs就是当前的代码段,ip 则是指令地址的 offset
    # 由于目前我们跳转到了 cs:ip = 0x90000,所以需要先把 ds es ss 等段寄存器设置为 0x9000
    # 设置 sp 栈指针为 0xFF00 的意思是,目前栈顶地址就是 ss:sp = 0x9FF00。是的, x86 中栈指针也是自顶向下的
    go:	mov	%cs, %ax		#将ds,es,ss都设置成移动后代码所在的段处(0x9000)
    	mov	%ax, %ds # ds 数据段
    	mov	%ax, %es # es 附加段 extra segment
    # put stack at 0x9ff00.
    	mov	%ax, %ss # ss 堆栈段
    	mov	$0xFF00, %sp		# arbitrary value >>512   # 堆栈指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10

    有两张图我觉得特别好,如下
    在这里插入图片描述

    在这里插入图片描述

    x86 架构由于历史原因,还保留着“段寄存器”的设计。每次我们访问内存和代码的时候,我们写在汇编指令里的地址,实际上都只是偏移,必须要加上段寄存器里的基址才可以正确访问内存。

    所以,go 这一小段代码

    go:	mov	%cs, %ax		#将ds,es,ss都设置成移动后代码所在的段处(0x9000)
    	mov	%ax, %ds # ds 数据段
    	mov	%ax, %es # es 附加段 extra segment
    # put stack at 0x9ff00.
    	mov	%ax, %ss # ss 堆栈段
    	mov	$0xFF00, %sp		# arbitrary value >>512   # 堆栈指针
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    的真正作用实际上就是在 CPU 跳转到 0x90000 后,设置好 ds es ss sp 等各个段寄存器和栈指针

    sp 要设置在 0xFF00 的原因就是栈指针离代码段离得越远越好。

    当然了,还有个问题

    TODO: 为什么要把 MBR 从 0x7c00 移动到 0x90000?直接在 0x7c00 搞事情不行吗?估计还得往后看看才知道

    继续看闪客文章第四回

    https://mp.weixin.qq.com/s?__biz=Mzk0MjE3NDE0Ng==&mid=2247499359&idx=1&sn=233812a464996b9566cdf3258132bc22&chksm=c2c584f2f5b20de40a7990c754cdbf3073b4652f318d479ac0c8ff686ca7aa74eef1ba7c6c2f&scene=178&cur_album_id=2123743679373688834#rd

    关于具体的代码,建议看闪客原本的文章,以及 Linux0.11 的相应源代码注释

    在对整个 Linux0.11 的编译构建阶段,会做这么一些事情:

    1. 把 bootsect.s 编译成 bootsect 放在硬盘的 1 扇区。
    2. 把 setup.s 编译成 setup 放在硬盘的 2~5 扇区。
    3. 把剩下的全部代码(head.s 作为开头)编译成 system 放在硬盘的随后 240 个扇区。

    随后在启动阶段时,bootsect.s 放在第一个扇区,作为 MBR,被 BIOS 加载到内存 0x7c00 处,随后 bootsect.s 再把 setup.s 和 system 加载到对应的内存位置 (0x90200 和 0x10000)

    整体流程如下图:
    在这里插入图片描述
    所以,总的来说,bootsect.s 存在的意义就是把 setup.s 和 内核 加载到内存。

    核心代码有两段,分别是 load_setup 和 ok_loadsetup,带注释的代码如下:

    # NOTE: load_setup 的作用是把 setup.s 加载到内存位置 0x90200
    ##ah=0x02 读磁盘扇区到内存	al=需要独出的扇区数量
    ##ch=磁道(柱面)号的低八位   cl=开始扇区(位0-5),磁道号高2位(位6-7)
    ##dh=磁头号					dl=驱动器号(硬盘则7要置位)
    ##es:bx ->指向数据缓冲区;如果出错则CF标志置位,ah中是出错码
    load_setup:
      # 从磁盘第二个扇区开始,读取四个扇区,存放到内存地址 0x90200 处
    	mov	$0x0000, %dx		# drive 0, head 0
    	mov	$0x0002, %cx		# sector 2, track 0  # dx 和 cx 的作用是指定要读取的扇区起始位置
    	mov	$0x0200, %bx		# address = 512, in INITSEG    # es:bx = 0x90200,是读取扇区存放的内存位置
    	.equ    AX, 0x0200+SETUPLEN # SETUPLEN = 4  
    	mov     $AX, %ax		# service 2, nr of sectors   # 读取磁盘内存,同时要读取 4 个扇区
    	int	$0x13			# read it # 发起 0x13 号中断,中断的参数为 ax, bx, cx, dx。这个中断处理程序是 BIOS 提前给我们写好的,是读取磁盘的相关功能函数
    
      # "jnc"指令的作用是检查进位标志位(Carry Flag)的状态。如果进位标志位为0,即没有进位发生,那么程序将会跳转到指定的目标地址执行。
      # int 0x13 在出错的时候会设置 CF=1,如果没有出错就不会设置 CF,因而 jnc 会跳转到 ok_load_setup
    	jnc	ok_load_setup		# ok - continue
    
      # 如果 CF=1,也就是读取磁盘失败,那么就会不断重复执行这段代码,也就是重试。
    	mov	$0x0000, %dx
    	mov	$0x0000, %ax		# reset the diskette
    	int	$0x13 
    	jmp	load_setup
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    ok_load_setup:
    
    # Get disk drive parameters, specifically nr of sectors/track
    
      # GUESS: 获取磁盘的一些参数
    	mov	$0x00, %dl
    	mov	$0x0800, %ax		# AH=8 is get drive parameters
    	int	$0x13
    	mov	$0x00, %ch
    	#seg cs
    	mov	%cx, %cs:sectors+0	# %cs means sectors is in %cs
    	mov	$INITSEG, %ax
    	mov	%ax, %es
    
    # Print some inane message
    
      # GUESS: 使用 BIOS 中断,重置光标在屏幕上的位置
    	mov	$0x03, %ah		# read cursor pos
    	xor	%bh, %bh
    	int	$0x10
    	
      # GUESS: 使用 BIOS 中断,在屏幕上打印 msg1: "IceCityOS is booting ..."
    	mov	$30, %cx
    	mov	$0x0007, %bx		# page 0, attribute 7 (normal)
    	#lea	msg1, %bp
    	mov     $msg1, %bp
    	mov	$0x1301, %ax		# write string, move cursor
    	int	$0x10
    
    # ok, we've written the message, now
    # we want to load the system (at 0x10000)
    
      # NOTE: 这是 ok_load_setup 的核心代码,作用是把从磁盘第六个扇区开始往后的 240 个扇区加载到内存 0x10000
    	mov	$SYSSEG, %ax # ax=0x1000
    	mov	%ax, %es		# segment of 0x010000 # es=0x1000
    	call	read_it   #
      # GUESS: 关掉跟软盘驱动相关的东西
    	call	kill_motor
    
    # After that we check which root-device to use. If the device is
    # defined (#= 0), nothing is done and the given device is used.
    # Otherwise, either /dev/PS0 (2,28) or /dev/at0 (2,8), depending
    # on the number of sectors that the BIOS reports currently.
    
    	#seg cs
    	mov	%cs:root_dev+0, %ax
    	cmp	$0, %ax
    	jne	root_defined
    	#seg cs
    	mov	%cs:sectors+0, %bx
    	mov	$0x0208, %ax		# /dev/ps0 - 1.2Mb
    	cmp	$15, %bx
    	je	root_defined
    	mov	$0x021c, %ax		# /dev/PS0 - 1.44Mb
    	cmp	$18, %bx
    	je	root_defined
    undef_root:
    	jmp undef_root
    root_defined:
    	#seg cs
    	mov	%ax, %cs:root_dev+0
    
    # after that (everyting loaded), we jump to
    # the setup-routine loaded directly after
    # the bootblock:
    
      # NOTE: 核心代码,在操作系统内核被加载到内存后,跳转到 0x90200 处,也就是 setup.s 代码开始的地方
    	ljmp	$SETUPSEG, $0
    
    • 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

    那么回到最初的问题:
    为了实现复杂的内核,毫无疑问我们需要使用 C 语言,需要从汇编跳转到 C 语言。由于 MBR 只有 512 字节,所以我们没法直接把内核写在 MBR,而是需要使用 MBR 加载内核到内存上。那么问题来了,现在我们已经把内核和setup.s 加载到内存上了。为什么不直接 jmp 到内核里的 main 函数,而是要先 jmp 到 setup.s 以及 head.s(system 的 entry)。它们做了些什么?这些事情可以被省略掉吗?我们还需要继续研究。

  • 相关阅读:
    三面拼多多都没过,惨败在(java中间件、数据库与spring框架)后悔没有早点知道这些
    text prompt如何超过77个词
    pycharm控制STM32F103ZET6拍照并上位机接收显示(OV7670、照相机、STM32、TFTLCD)
    界面控件DevExpress WPF拥有丰富SVG图像库,更好支持高DPI显示器
    Android NDK之使用 arm-v7a 汇编实现两数之和
    G口服务器的作用是什么?
    金仓数据库 KingbaseES 插件参考手册 xml2
    Nacos使用实践
    Vue2.0开发之——Vue基础用法-事件绑定$event(20)
    行业专网对比公网,优势在哪儿?能满足什么特定要求?
  • 原文地址:https://blog.csdn.net/shimly123456/article/details/133150126