02|从 0x7c00 到 0x90000
你好,我是闪客。
你可能会问,这一讲操作系统是不是就该做一些更高深的事情了?想多了,完全没有,等你读完这一讲后,你就知道了。
上一讲,我们讲了CPU 执行操作系统的最开始的两行代码:
mov ax,0x07c0
mov ds,ax
这两行代码将数据段寄存器 ds 的值变成了 0x07c0,方便之后访问内存时,利用这个段基址进行寻址。
接下来我们带着这两行代码,继续往下看 6 行,代码如下:
mov ax,0x9000
mov es,ax
mov cx,#256
sub si,si
sub di,di
rep movw
此时,ds 寄存器的值已经是 0x07c0 了,然后又通过同样的方式将 es 寄存器的值变成 0x9000,接着又把 cx 寄存器的值变成 256(代码里确实是用十进制表示的,与其他地方有些不一致,不过不影响)。
现在 ds、es和cx 寄存器的值就都被赋上了确定的值了,我们先接着往下看。
再往下看有两个 sub 指令:
sub si,si
sub di,di
这个 sub 指令很简单,比如:
sub a,b
表示的意思是:
a = a - b
那么这行代码:
sub si,si
就表示:
si = si - si
也就是说,如果 sub 后面的两个寄存器一模一样,就相当于把这个寄存器里的值清零,这是一个基本玩法。
这样一来,理解起来就非常简单了。经过这些指令后,以下几个寄存器分别被赋上了指定的值:
ds = 0x07c0
es = 0x9000
cx = 256
si = 0
di = 0
还记得上一讲画的 CPU 寄存器总图么?经过赋值之后,情况就变成了这样:

看到这里,你可能有个疑问:我们干嘛要给这些毫不相干的寄存器赋上值呢?其实这个操作就是为下一条指令服务的,指令是这样的:
rep movw
其中 rep 表示重复执行后面的指令,而后面的指令 movw 表示复制一个字(word 16位),其实就是不断重复地复制一个字。
根据这行指令,自然就会引出以下三个问题:
1.重复执行多少次呢?答案是 cx 寄存器中的值,也就是 256 次。
2.从哪复制到哪呢?答案是从 ds:si 处复制到 es:di 处,也就是从 0x7c00 复制到 0x90000。
3.一次复制多少呢?刚刚说过了,复制一个字16 位,也就是两个字节。那么。一共复制 256 次的两个字节,其实就是复制 512 个字节。
好了,总结一下就是,将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处开始的后面 512 字节的地方,也就是下图的第二步:

没错,就是这么折腾了一下。现在,操作系统最开头的代码已经被挪到了 0x90000 这个位置了。再往后是一个跳转指令了,代码如下:
jmpi go,0x9000
go:
mov ax,cs
mov ds,ax
仔细想想,或许你能猜到它是做什么的。jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行。
还记得上一讲说的段基址 : 偏移地址这种格式的内存地址要如何计算吗?段基址仍然要先左移四位再加上偏移地址,段基址 0x9000 左移四位就是 0x90000,因此结论就是跳转到 0x90000 + go 这个内存地址处执行。如果你觉得陌生,可以回看第1讲的内容,我们要稳扎稳打。
我们再说说 go 是个啥东西。go 就是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。当然更准确的说法是,bootsect.s 编译成二进制文件 bootsect 后,go 这个标签在被编译成二进制文件里的内存地址偏移量。
这个偏移地址再加上 0x90000,就刚好是 go 标签处的那行代码 mov ax,cs 此时所在的内存地址了。

假如 mov ax,cs 这行代码位于最终编译好的二进制文件的 0x08 处,那 go 就等于 0x08,而最终 CPU 跳转到的地址就是 0x90008 处。
总结
好,我们来总结一下前2讲的内容。其实就是一段 512 字节的代码和数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到 0x90000 加上 go 这个标签所代表的偏移量,也就是 mov ax,cs 这行指令的位置。
接下来的一讲,我们就把目光定位到 go 标签处的代码,看看它又要“折腾”些什么吧。
好,我是闪客。后面的世界越来越精彩,欲知后事如何,且听下回分解。