从0x7c00到0x90000
前言提要
前文讲了 CPU 执行操作系统的最开始的两行代码
mov ax,0x07c0
mov ds,ax
这两行代码将数据段寄存器 ds 的值变成了 0x07c0,方便之后访问内存时,利用这个段基址进行寻址。
本节问题
- 从0x7c00到0x90000这之间发生了什么?
从ES:SI地址复制到DS:DI地址
在代码的第六行:
// 把值0x9000移动到寄存器ax中
mov ax,0x9000
// 把ax中的值移动到寄存器es中。
mov es,ax
// 把值256移动到寄存器cx中
mov cx,#256
// 把寄存器si的值设置为0
sub si,si
// 把寄存器di的值设置为0
sub di,di
// 重复执行"movw"指令,直到cx为0
rep movw
因此,这些汇编代码将会把ES:SI地址开始的256个字笻(word)复制到DS:DI地址开始的位置. 此时,各寄存器的值如下所示:
ds寄存器的值为0x07c0 es寄存器的值为0x9000 cx寄存器的值为256 si寄存器的值为0 di寄存器的值为0
小问一:sub指令是什么?
SUB 指令是汇编语言中的一种运算指令,它用来执行减法运算。SUB 指令的格式一般是 "SUB destination, source",其中 destination 是被减数,source 是减数。
在执行 SUB 指令时,会把 destination 和 source 中的数值相减,并将结果存储在 destination 中。该指令可以用来更新寄存器和内存位置中的数值。
例如:
sub a,b
表示的意思是:
a = a - b
SUB 指令还可以用来执行一些特殊操作,如下面两个例子。
"SUB AX, BX" 把 AX 中的值减去 BX 中的值,并将结果存储在 AX 中
"SUB [SI], DX" 把内存地址 DS:SI 中的值减去 DX 中的值,并将结果存储回该内存地址
扩展:"SUB"有一个变体 "CMP" ,它也是一个减法指令, 但是它不会更新寄存器的值,而是把结果存储在标志寄存器中,用来判断结果是否为0,是否大于或小于0。(限于笔记长度,这里不做过多讨论,感兴趣的小伙伴可以上网搜搜相关文章)
对于SUB指令来说, 一般会改变结果寄存器的值, 但是会根据减法结果来更新一些标志位,例如:
- CF (Carry flag) 进位标志,当减法结果小于0时设置为1。
- SF (Sign flag) 符号标志,当减法结果小于0时设置为1。
- ZF (Zero flag) 零位,当减法结果等于0时设置为1。
- OF (Overflow flag) 溢出标志,当减法结果溢出时设置为1。 这些标志位可以在之后的指令中用来做一些判断和跳转操作,例如: JG (Jump if greater) 指令可以在ZF=0并且SF=OF的情况下跳转,这说明了结果大于0.
最后需要提醒的是, SUB指令不会处理符号位,如果需要处理有符号数,需要使用SBB指令 (Subtract with borrow)。
小问二:(扩展)什么是标志位?什么是符号位?
标志位是CPU的一种特殊寄存器,用来保存运算结果的状态,帮助判断程序的运行状态。标志位可以被程序读取和修改,是程序控制和流程控制的重要部分。
符号位是标志位中的一种,用来表示数字的正负性。在二进制补码中,最高位(最左边)为1表示负数,0表示正数。如果符号位为1,则这个数为负数;如果符号位为0,则这个数为正数。
在一些CPU中,有一个单独的标志位来表示符号位,常用的名称为 SF (Sign flag)。当运算结果的符号位为1时,SF标志位被置1,否则被置0。
标志位可以被一些特殊的指令(如JG, JL, JE)读取并用来做条件判断,从而实现控制程序的流程。
我们为什么要给这些寄存器赋值?
简单来说,从mov ax,0x9000到sub di,di这段代码都是为rep movw服务的
mov ax,0x9000和mov es,ax指令将0x9000值赋值给了es寄存器,它会被用来设置源地址的段地址。
mov cx,#256指令将256值赋值给了cx寄存器,它会被用来设置拷贝的字节数。
sub si,si和sub di,di指令将si和di寄存器的值设置为0,这两个寄存器可能被用来设置源地址和目标地址的偏移量。
rep movw指令会复制es:si和ds:di地址开始的256个字符(word)。rep指令是一种循环执行指令的指令,在这里的rep movw指令将会按照cx计数器的值来重复执行movw指令,直到cx为0为止.
小问一:重复执行多少次呢?
答:cx寄存器中的值,即256次
小问二:从哪复制到哪呢?
答:从 ds:si 处复制到 es:di 处,也就是从 0x7c00 复制到 0x90000
小问三:一次复制多少呢?
答:复制一个字 16 位,也就是两个字节。那么。一共复制 256 次的两个字节,其实就是复制 512 个字节。
小结
将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处开始的后面 512 字节的地方
sequenceDiagram
participant Memory as Memory
participant 0x7c00 as A
participant 0x90000 as B
Memory ->> A: Read 512 bytes starting at address 0x7c00
A ->> B: Write 512 bytes starting at address 0x90000
现在操作系统最开始的代码已经在 0x9000 位置了,再往后是一个跳转指令:
jmpi go,0x9000
go:
mov ax,cs
mov ds,ax
代码解释:
jmpi go,0x9000: 这条指令是一个跳转指令,它将程序的执行流程跳转到标号为go的位置。 0x9000是一个地址,表示跳转到0x9000地址处继续执行
go: 这是一个标号,表示0x9000地址处的代码。
mov ax,cs: 这条指令将当前代码段寄存器(CS)的值存入ax寄存器。
mov ds,ax: 这条指令将ax寄存器中的值存入数据段寄存器(DS)。
“段基址 : 偏移地址”这种格式的内存地址要如何计算?
在 x86 架构中 ,内存地址通常由段基址和偏移地址组成。段基址表示段在内存中的起始地址,偏移地址表示相对于段基址的偏移量。
在x86架构中,偏移地址格式的内存地址是通过将段基址左移4位然后加上偏移地址来计算的。例如:
段基址 : 偏移地址 = 段基址 * 16 + 偏移地址
0x9000 : 0x0500 = 0x90000 + 0x0500 = 0x90500
在上文中,go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。
这个偏移地址再加上 0x90000,就刚好是 go 标签处的那行代码 mov ax,cs 此时所在的内存地址了。
注意,在x86_64架构中,由于没有段寄存器,所以没有段基址这个概念,没有左移4位的操作。
总结
本文的代码实现了如下操作:
将一段 512 字节的代码和数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到 0x90000 加上 go 这个标签所代表的偏移量,也就是 mov ax,cs 这行指令的位置。