明确:
1、编译器将程序翻译成了机器指令,CPU执行的是机器指令。
2、操作系统本身也是个程序,执行之后会生成一大堆机器指令。
回顾:当按下开机键的那一刻,在主板上提前写死的固件程序 BIOS 会将硬盘中启动区的 512 字节的数据,原封不动复制到内存中的 0x7c00 这个位置,并跳转到那个位置进行执行。(只要硬盘中的 0 盘 0 道 1 扇区的 512 个字节的最后两个字节分别是 0x55 和 0xaa,那么 BIOS 就会认为它是个启动区。)所以作为操作系统的开发人员,仅需要把操作系统最开始的那段代码,编译并存储在硬盘的 0 盘 0 道 1 扇区即可。之后搬运工BIOS就会把它放到内存去跳转执行。
Linux-0.11 的最开始的代码,就是这个用汇编语言写的 bootsect.s,位于 boot 文件夹下。通过编译,这个 bootsect.s 会被编译成二进制文件,存放在启动区的第一扇区。
随后就会由 BIOS 搬运到内存的 0x7c00 这个位置,而 CPU 也会从这个位置开始,不断往后一条一条语句无脑地执行下去。之后的一切将从这两行代码开始:
1 2 |
|
这段代码是用汇编语言写的,含义是把 0x07c0 这个值复制到 ax 寄存器里,再将 ax 寄存器里的值复制到 ds 寄存器里。那其实这一番折腾的结果就是,让 ds 这个寄存器里的值变成了 0x07c0。
当我们写一段汇编语言时,实际上仅仅写了偏移地址,比如:
1 |
|
相当于
1 |
|
ds是一个16位的段寄存器,具体表示数据段寄存器,在内存寻址时充当段基址的作用。ds 是默认加上的,表示在 ds 这个段基址处,往后再偏移 0x0001 单位,将这个位置的内存数据,复制到 ax 寄存器中。
这个 ds 被赋值为了 0x07c0,由于 x86 为了让自己在 16 位这个实模式下能访问到 20 位的地址线这个历史因素,所以段基址要先左移四位。那 0x07c0 左移四位就是 0x7c00,那这就刚好和这段代码被 BIOS 加载到的内存地址 0x7c00 一样了。也就是说,之后再写的代码,里面访问的数据的内存地址,都先默认加上 0x7c00,再去内存中寻址。
因为BIOD规定死了把操作系统代码加载到内存 0x7c00,所以里面的各种数据都被偏移了这么多,所以把数据段寄存器ds设置为这个值,方便了之后访问内存时利用这个段基址进行寻址。
之后的代码:
1 2 3 4 5 6 7 8 |
|
sub si,si意思位si=si - si,si是寄存器,这个举动将寄存器里的值清零。此时几个寄存器分别被赋初值
赋值为了执行:
1 |
|
意思是 “重复执行后面的指令 字(16位)",执行cx=256次,从 ds:si 处复制到 es:di 处,一次复制两个字节。这条命令就是为了将内存地址 0x7c00 处开始往后的 512 字节的数据,原封不动复制到 0x90000 处。
后面接一个跳转指令
1 2 3 4 |
|
jmpi 是一个段间跳转指令,表示跳转到 0x9000:go 处执行。go 是一个标签,最终编译成机器码的时候会被翻译成一个值,这个值就是 go 这个标签在文件内的偏移地址。这个偏移地址再加上 0x90000,就刚好是 go 标签后面那段代码 mov ax,cs 此时所在的内存地址了。
那假如 mov ax,cx 这行代码位于最终编译好后的二进制文件的 0x08 处,那 go 就等于 0x08,而最终 CPU 跳转到的地址就是 0x90008 处。
总结:一段 512 字节的代码和数据,从硬盘的启动区先是被移动到了内存 0x7c00 处,然后又立刻被移动到 0x90000 处,并且跳转到此处往后再稍稍偏移 go 这个标签所代表的偏移地址处,也就是 mov ax,cs 这行指令的位置。简称”自己给自己挪地方“ 为甚末跳转?
这些指令都是cpu中的
1 2 3 4 5 |
|
这段代码的直意思就是把 cs 寄存器的值分别复制给 ds、es 和 ss 寄存器,然后又把 0xFF00 给了 sp 寄存器。cs 寄存器表示代码段寄存器,CPU 当前正在执行的代码在内存中的位置,就是由 cs:ip 这组寄存器配合指向的,其中 cs 是基址,ip 是偏移地址。
其实操作系统在做的事情,就是给如何访问代码,如何访问数据,如何访问栈进行了一下内存的初步规划。其中访问代码和访问数据的规划方式就是设置了一个基址而已,访问栈就是把栈顶指针指向了一个远离代码位置的地方而已。