我们的目标

在本节中我们将让系统从软盘启动并在屏幕上打印“Hello World!”。

下载代码


内存访问

处理器把内存当做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

asld是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。

making process of first cry

make完成后,我们应该得到一个映像文件final.img,应该正好512字节长。

请在VMWARE按照如下设置建立一个虚拟机,

vmware machine configuration

最重要的部分是它必须有4MB内存和在总线0:0处有100MB的IDE硬盘。为了除去检测硬件的需求以简化代码流程,这些值被硬编码到代码中。

让VMWARE从软盘镜像final.img引导。然后启动虚拟机,你应该啥也看不到……

first cry result: black screen

这个结果是正确的,因为我们就让它在原地蹦。


Hello World!

好吧,我必须承认在初啼中的程序不好玩,所以嘛按照惯例让我们输出"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

DSES赋予正确的段值,寄存器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,

Hello world reuslt


你可以自由使用我的代码,如有疑问请联系我