处理器把内存当做8位字节的序列来管理和存取,每一个内存字节都有一个对应的地址:物理地址,用地址可以表示的长度叫做寻址空间。
两种通用的内存寻址方式:分段和分页。Skelix两种都用。
在美好的旧日时光:DOS时代,我们应该很熟悉分段了。因为当时所有的寄存器都是16位的,所以我们只能直接存取2^16 = 65536字节的内存空间。 对程序员来说64KB是绝对不够用的。Intel使用两个寄存器通过段:偏移的方式来表示一个物理地址,它使用一个16为的段寄存器来表示一个内存段 和另一个16位的寄存器表示在这个段中的偏移。这是一个听起来很不错的方案,它不仅能把我们的代码、数据和堆栈放在不同的段中以防止它们 互相覆盖,而且能给我们存取2^16*2^16 = 4G字节内存空间的能力。好得都不像真的了,确实不是真的。
这个有点巧妙,段寄存器的值必须左移4位,然后与偏移寄存器的值相加,得到的才是物理地址。例如,7C00:0189这一对表示的是物理地址7C189而不是 7C000189。注意所有的内存地址都是十六进制表示的,
7C000
+ 0189
-------
7C189
很明显,它能表示的最大值是FFFF:FFFF
FFFF0
+ FFFF
------
10FFEF
也就是1M + 65519字节, 因为80386使用20位的内存地址总线(随后讨论),因此超出的65519字节的内存空间被绕回到了物理地址0.比如地址100010映射到了地址10, 存取100010就和存取10一样。
这个方案还产生了另外一个问题,就是可以有不同的方法表示同一个物理地址,比如07C0:0000和0000:7C00都表示物理地址0007C00。
另一个内存寻址的方案是线性地址方案,这个方案使用32位线性地址,后面的教程中会讨论它。
加电或重启后,处理器会被初始化,它将寄存器设置成已知的状态,处理器被设置成实模式。接着处理器执行处于物理地址FFFFFFF0的一条指令,通常是一个由EPROM设置的far JMP
。
你可能好奇段:偏移怎么能够表示物理地址FFFFFFF0,实际上寄存器CS
有一个不可见部分,它存储了基地址FFFF0000,在重置时IP
的值是
FFF0,它们相加后的值是FFFF0000+FFF0 = FFFFFFF0。接着BIOS会初始化总线、端口之类的东西。
一旦BIOS结束了初始化,它会试图载入操作系统。因为它不知道你用的系统是什么样子的,所以它只是读取启动盘的第一个扇区到一个预定义的地址去,这个地址是物理内存00007C00。 从00007C00开始的指令应该建立一个指定操作系统所需的合适环境。
总而言之,我们需要一个启动盘上的一个512字节扇区,并且BIOS要求必须以AA55结尾,它标志了这是一个有效的启动扇区。
Skelix使用软盘启动。
现在需要知道的是,在启动时,处理器处于实模式,使用段:偏移进行没有特权级保护的1MB以内内存寻址。在这个阶段我们可以使用BIOS中断。
下面看一下第一个代码段,
01/first.cry/bootsect.s
.text
.globl
start
.code16
.text
标识了代码区的开始。
.globl
start
告诉汇编器start
是一个外部链接。
GCC默认使用32位操作数和地址,.code16
告诉GCC使用16位操作数和地址模式。
start:
jmp
start
原地蹦。
.org
0x1fe, 0x90
.word
0xaa55
.org
0x1fe, 0x90
表示把从原地蹦指令到1FE(十进制510)之间的空隙以十六进制码90(汇编指令NOP
,啥也不干)填充。如前所述,把AA55写在512位扇区的最后,这是启动扇区必须的标志。
现在我们有了第一个代码文件,必须先把它make出来。为了把过程自动化,我们需要一个Makefile文件。我不准备详细解释怎么写Makefile文件,你可以上网搜一下。我将专注于解释编译选项。
01/first.cry/Makefile
AS=as
LD=ld
as
和ld
是GCC工具链使用的汇编器和连接器。
.s.o:
${AS} -a $< -o $*.o >$*.map
all: final.img
final.img: bootsect
mv bootsect final.img
bootsect: bootsect.o
${LD} --oformat binary -N -e start -Ttext 0x7c00 -o bootsect $<
--oformat binary
表示要GCC产生一个纯粹的没有文件头和其它信息的“平坦”二进制文件,就想DOS下的.com一样。没有这个选项,ld
默认使用ELF格式(取决于你的系统设置),但BIOS可不认得ELF是什么东西。
在这段代码里你可能不需要-N
这个选项,单位了将来的方便还是放在这里。它让代码区可读可写,因为我不设置单独的数据区,在后面的章节中我将会在代码区中执行写入操作。
-e start
命名一个入口点,它告诉连接器应该在start
处开始执行代码。
-Ttext 0x7c00
将代码区的基地址设置为7C00,它是引导扇区被载入内存的初始地址。代码区中所有的代码地址都将被加上7C00。例如start
的地址将是7C00,而结束标识AA55的地址是7C00+1FE = 7DFE。
make完成后,我们应该得到一个映像文件final.img
,应该正好512字节长。
请在VMWARE按照如下设置建立一个虚拟机,
最重要的部分是它必须有4MB内存和在总线0:0处有100MB的IDE硬盘。为了除去检测硬件的需求以简化代码流程,这些值被硬编码到代码中。
让VMWARE从软盘镜像final.img
引导。然后启动虚拟机,你应该啥也看不到……
这个结果是正确的,因为我们就让它在原地蹦。
好吧,我必须承认在初啼中的程序不好玩,所以嘛按照惯例让我们输出"Hello World"吧。
01/hello.world/bootsect.s
.text
.globl
start
.code16
start:
jmp
code
msg:
.string
"Hello World!\x0"
code:
movw
$0xb800,%ax
movw
%ax
, %es
xorw
%ax
, %ax
movw
%ax
, %ds
把DS
和ES
赋予正确的段值,寄存器ES
指向段B800,如前所述,它指向从B8000开始的内存空间,这是彩色显卡的视频内存地址。这个内存区域的改变将直接影响屏幕显示,例如在一个80列25行的显示器上,处于0列0行的字符指向内存地址B8000,它的色彩属性指向地址B8001。如果我们改变B8000的值为0x31,就是ASCII的1,改变B8001的值为0x07,那么我们将会看到一个黑底白字的1显示在屏幕的左上角。
movw
$msg, %si
xorw
%di
, %di
cld
movb
$0x07, %al
1:
cmp
$0, (%si
)
je
1f
movsb
stosb
jmp
1b
将字符串“Hello World!”和它相应的色彩属性填充到B8000开始的内存区域,色彩属性是存于AL
中的值7,它表示黑底白字。
1: jmp
1b
.org
0x1fe, 0x90
.word
0xaa55
我们还是使用初啼中的那个Makefile,
你可以自由使用我的代码,如有疑问请联系我。