3.1.2 as86汇编语言程序
下面以一个简单的框架示例程序boot.s来说明as86汇编程序的结构以及程序中语句的语法,然后给出编译链接和运行方法,最后分别列出as86和ld86的使用方法和编制选项。示例程序如下:
1 ! 2 ! boot.s -- bootsect.S的框架程序。用代码0x07替换串msg1中1字符,然后在屏幕第1行上显示。 3 ! 4 .globl begtext,begdata,begbss,endtext,enddata,endbss ! 全局标识符,供ld86链接使用。 5 .text ! 正文段。 6 begtext: 7 .data ! 数据段。 8 begdata: 9 .bss ! 未初始化数据段。 10 begbss: 11 .text ! 正文段。 12 BOOTSEG = 0x07c0 ! BIOS加载bootsect代码的原始段地址。 13 14 entry start ! 告知链接程序,程序从start标号处开始执行。 15 start: 16 jmpi go,BOOTSEG ! 段间跳转。BOOTSEG指出跳转段地址,标号go是偏移地址。 17 go: mov ax,cs ! 段寄存器cs值-->ax,用于初始化数据段寄存器ds和es。 18 mov ds,ax 19 mov es,ax 20 mov [msg1+17],ah ! 0x07-->替换字符串中1个点符号,喇叭将会鸣一声。 21 mov cx,#20 ! 共显示20个字符,包括回车换行符。 22 mov dx,#0x1004 ! 字符串将显示在屏幕第17行、第5列处。 23 mov bx,#0x000c ! 字符显示属性(红色)。 24 mov bp,#msg1 ! 指向要显示的字符串(中断调用要求)。 25 mov ax,#0x1301 ! 写字符串并移动光标到串结尾处。 26 int 0x10 ! BIOS中断调用0x10,功能0x13,子功能01。 27 loop0: jmp loop0 ! 死循环。 28 msg1:.ascii "Loading system ..."! 调用BIOS中断显示的信息。共20个ASCII码字符。 29 .byte 13,10 30 .org 510 ! 表示以后语句从地址510(0x1FE)开始存放。 31 .word 0xAA55 ! 有效引导扇区标志,供BIOS加载引导扇区使用。 32 .text 33 endtext: 34 .data 35 enddata: 36 .bss 37 endbss: |
这个示例是bootsect.S的一个框架程序,能编译生成引导扇区代码。其中为了说明某些语句的使用方法,特意加入了无意义的第20行语句。
我们首先介绍该程序的功能,然后详细说明各语句的作用。该程序是一个简单的引导扇区启动程序。编译链接产生的执行程序可以放入软盘第1个扇区直接用来引导计算机启动。启动后会在屏幕第17行第5列处显示出红色字符串"Loading system…",并且光标下移一行。然后程序就在第27行上死循环。
该程序开始的3行是注释语句。在as86汇编语言程序中,凡是以感叹号"!"或分号";"开始的语句其后面均为注释文字。注释语句可以放在任何语句的后面,也可以从一个新行开始。
第4行上的".globl"是汇编指示符(或称为汇编伪指令、伪操作符)。汇编指示符均以一个字符"."开始,并且不会在编译时产生任何代码。汇编指示符由一个伪操作码,后跟0个或多个操作数组成。例如第4行上的"globl"是一个伪操作码,而其后面的"begtext, begdata, begbss"等标号就是它的操作数。标号是后面带冒号的标识符,如第6行上的begtext:。但是在引用一个标号时无须带冒号。
通常,各汇编器都支持很多不同的伪操作符,但是下面仅说明Linux系统bootsect.S和setup.s汇编语言程序用到的和一些常用的as86伪操作符。
.globl伪操作符用于定义随后的标号标识符是外部的或全局的,并且即使不使用也强制引入。
第5行~11行上除定义了3个标号外,还定义了3个伪操作符:.text、.data、.bss。它们分别对应汇编程序编译产生目标文件中的3个段,即正文段、数据段和未初始化数据段。.text用于标识正文段的开始位置,并把当前段切换到text段;.data用于标识数据段的开始位置,并把当前段切换到data段;而.bss则用于标识一个未初始化数据段的开始,并把当前段改变成bss段。因此行5~11用于在每个段中定义一个标号,最后再切换到text段开始编写随后的代码。这里把3个段都定义在同一重叠地址范围中,因此本示例程序实际上不分段。
第12行定义了一个赋值语句"BOOTSEG = 0x07c0"。等号"="(或符号EQU)用于定义标识符BOOTSEG所代表的值,因此这个标识符可称为符号常量。这个值与C语言中的写法一样,可以使用十进制、八进制和十六进制。
第14行上的标识符entry是保留关键字,用于迫使链接器ld86在生成的可执行文件中包括进其后指定的标号start。通常在链接多个目标文件生成一个可执行文件时应该在其中一个汇编程序中用关键词entry指定一个入口标号,以便调试。但是在我们这个示例中以及Linux内核boot/bootsect.S和boot/setup.s汇编程序中完全可以省略这个关键词,因为我们并不希望在生成的纯二进制执行文件中包括任何符号信息。
第16行上是一个段间(Inter-segment)远跳转语句,就跳转到下一条指令。由于当BIOS把程序加载到物理内存0x7c00处并跳转到该处时,所有段寄存器(包括CS)默认值均为0,即此时CS:IP=0x0000:0x7c00。因此这里使用段间跳转语句就是为了给CS赋段值0x7c0。该语句执行后CS:IP = 0x07C0:0x0005。随后的两条语句分别给DS和ES段寄存器赋值,让它们都指向0x7c0段。这样便于对程序中的数据(字符串)进行寻址。
第20行上的MOV指令用于把ah寄存器中0x7c0段值的高字节(0x07)存放到内存中字符串msg1最后一个"."位置处。这个字符将导致BIOS中断在显示字符串时鸣叫一声。使用这条语句主要是为了说明间接操作数的用法。在as86中,间接操作数需要使用方括号对。另外一些寻址方式如下:
! 直接寄存器寻址。跳转到bx值指定的地址处,即把bx的值复制到IP中。 mov bx,ax jmp bx ! 间接寄存器寻址。bx值指定内存位置处的内容作为跳转的地址。 mov [bx],ax jmp [bx] ! 把立即数1234放到ax中。把msg1地址值放到ax中。 mov ax,#1234 mov ax,#msg1 ! 绝对寻址。把内存地址1234(msg1)处的内容放入ax中。 mov ax,1234 mov ax,msg1 mov ax,[msg1] ! 索引寻址。把第2个操作数所指内存位置处的值放入ax中。 mov ax,msg1[bx] mov ax,mgs1[bx*4+si] |
第21~25行的语句分别用于把立即数放到相应的寄存器中。立即数前一定要加井号"#",否则将作为内存地址使用而使语句变成绝对寻址语句,见上面示例。另外,把一个标号(如msg1)的地址值放入寄存器中时也一定要在前面加"#",否则会变成把msg1地址处的内容放到寄存器中。
第26行是BIOS屏幕显示中断调用int 0x10。这里使用其功能19、子功能1。该中断的作用是把一字符串(msg1)写到屏幕指定位置处。寄存器cx中是字符串长度值,dx中是显示位置值,bx中是显示使用的字符属性,es:bp指向字符串。
第27行是一个跳转语句,跳转到当前指令处。因此这是一个死循环语句。这里采用死循环语句是为了让显示的内容能够停留在屏幕上而不被删除。死循环语句是调试汇编程序时常用的方法。
第28~29行定义了字符串msg1。定义字符串需要使用伪操作符.ascii,并且需要使用双引号括住字符串。伪操作符.asciiz还会自动在字符串后添加一个NULL(0)字符。另外,第29行上定义了回车和换行(13,10)两个字符。定义字符需要使用伪操作符.byte,并且需要使用单引号把字符括住,如"'D'"。当然我们也可以像示例中一样直接写出字符的ASCII码。
第30行上的伪操作符语句.org定义了当前汇编的位置。这条语句会把汇编器编译过程中当前段的位置计数器值调整为该伪操作符语句上给出的值。对于本示例程序,该语句把位置计数器设置为510,并在此处(第31行)放置了有效引导扇区标志字0xAA55。伪操作符.word用于在当前位置定义一个双字节内存对象(变量),其后可以是一个数或者是一个表达式。由于后面没有代码或数据了,因此我们可以据此确定boot.s编译出来的执行程序应该正好为512B。
第32~37行又在3个段中分别放置了3个标号。分别用来表示3个段的结束位置。这样设置可以用来在链接多个目标模块时区分各个模块中各段的开始和结束位置。由于内核中的bootsect.S和setup.s程序都是单独编译链接的程序,各自期望生成的都是纯二进制文件而并没有与其他目标模块文件进行链接,因此示例程序中声明各个段的伪操作符(.text、.data和.bss)都完全可以省略,即把程序中第4~11行和32~37行全部删除也能编译链接产生出正确的结果。