【我所認知的BIOS】->汇编语言之宏汇编1By LightSeed2010-2-2 其实早就想写点关于汇编语言的文章了,但是最近感觉比较累,自己也比较懒今天才动手写。哎。。。真是身心俱疲,房价涨了,小菜也涨了,妹儿的要求也涨了,但是哥的工资还是不涨。这社会我快混不下去了。但是想想做男人嘛,有什么大不了的。做人,最重要的是心态要好,要稳。1、我体会到的C和汇编目前我还做的legacy的BIOS,里面全是汇编语言。老实说,最近在study C语言的东西,也在研究用C生成ASM的文件,细细想来C确实很强大,汇编语言的效率确实也高。从C生成的汇编语言来看的话,如果是组织的好,实际的汇编语言的效率应该是比C高的。关于汇编语言的基础性的东西我就不多赘述了。在这里我想谈谈我在仿写ADU那个debug tool的时候用的一些汇编语言的里的高级语句。(当然先说清楚,这也是参考了BIOS code的东西,让我自己写我想我是写不出来的啦。),话有说回来,对于程序员来说,如果让我在C和汇编语言之间选择,当然还是比较喜欢用C了。(最近我在study ACPI的东西的时候,就试图用C来写一个ACPI dump的工具,发觉用C写真的好方便哦,两天就能搞出来一个功能了,但是如果用汇编的话,就算知道算法,估计写起来也比较费神。)在组织数据段中的数据的时候,按照结构体的形式来存储有关联的数据是一种明智的选择。但是由于这个结构体需要输入的东西比较多,那么每次都手动去输入相关数据的话,估计烦也烦死了。所以宏汇编在这里就其了很大的作用。2、宏汇编(以下这段话是引用自网上的,对于这些定义放在这里纯粹是为了这篇文章的完整性。);----------------------------引用开始----------------------在程序设计中,为了简化程序的设计,将多次重复使用的程序段用宏指令代替。宏指令是指程序员事先定义的特定的“指令”,这种“指令”是一组重复出现的程序指令块的缩写和替代。宏指令定义后,凡在宏指令出现的地方,宏汇编程序总是自动地把它们替换成对应的程序指令块。宏指令有时也称为宏,包含宏定义和宏调用。使用宏指令的好处是:简化源程序的编写,传递参数灵活,功能更强。2.1宏指令的定义宏是源程序中的一段具有独立功能的程序代码。它只要在源程序中定义一次,就可以多次调用,调用时只要使用一个宏指令语句就可以了。宏指令定义由开始伪指令MACRO、宏指令体、宏指令定义结束伪指令ENDM组成。格式如下:宏指令名 MACRO [形式参数1,形式参数2,…,形式参数N];宏指令体(宏体)ENDM其中,宏指令名是宏定义为宏体程序指令块规定的名称,可以是任一合法的名字,也可以是系统保留字(如指令助记符、伪指令操作符等),当宏指令名是某个系统保留字时,则该系统保留字就被赋予新的含义,从而失去原有的意义。MACRO语句到ENDM语句之间的所有汇编语句构成宏指令体,简称宏体,宏体中使用的形式参数必须在MACRO语句中列出。形式参数是出现在宏体内某些位置上可以变化的符号,也可以是任一合法的名字,甚至是寄存器名(笔者:我不建议你这样使用)。如果形式参数中使用某些寄存器名,那么在宏汇编展开时,将不认为这些寄存器名是寄存器本身,而是形式参数,并被实际参数所代替。形式参数可以缺省,也可以有一个或多个。当形式参数多于一个时,形式参数之间用逗号隔开。定义宏指令时,每行的字符数不能多于132个。宏指令定义一般放在源程序的开头,以避免产生不应发生的错误。宏指令必须先定义后调用(引用)。宏指令可以重新定义,也可以嵌套定义。嵌套定义是指在宏指令体内还可以再定义宏指令或调用另一宏指令。(笔者:其实这和C中的方式也比较相似。扩展一下,汇编语言中的结构体也是这也的。)2.2宏指令的调用宏指令一旦定义后,就可以用宏指令名字(宏名)来引用(或调用),这个引用过程即为宏调用。宏调用的格式为宏指令名 实际参数1,实际参数2,…,实际参数N其中,“实际参数”的类型和顺序要与形式参数的类型和顺序保持一致,宏调用时将一一对应地替换宏指令体中的形式参数。当有两个以上参数时,中间用逗号、空格或制表符隔开。宏指令调用时,实际参数的数目并不一定要和形式参数的数目一致。当实参个数多于形参的个数时,忽略多余的实参;当实参个数少于形参个数时,多余的形参用空串代替。;---------------------------------引用结束------------------------哈哈,我们应该向前辈看齐的嘛,多学习前辈的东西,总是不会错的。3、条件汇编IF/IFE伪指令中的表达式可以采用第3章学习的关系运算符EQ(相等)、NE(不相等)、GT(大于)、LT(小于)、GE(大于等于)、LE(小于等于)。关系表达式用0ffffh(非0)表示真,用0表示假。 条件汇编伪指令格式功能说明IF表达式汇编程序求出表达式的值,此值不为0则条件满足IFE表达式汇编程序求出表达式的值,此值为0则条件满足IFDEF符号符号已定义(内部定义或声明外部定义),则条件满足IFNDEF符号符号未定义,则条件满足IFB <形参>用在宏定义体。如果宏调用没有用实参替代该形参,则条件满足IFNB <形参>用在宏定义体。如果宏调用用实参替代该形参,则条件满足IFIDN <字符串1>,<字符串2>字符串1与字符串2相同则条件满足;区别大小写IFIDNI <字符串1>,<字符串2>字符串1与字符串2相同则条件满足;不区别大小写IFDIF <字符串1>,<字符串2>字符串1与字符串2不相同则条件满足;区别大小写IFDIFI <字符串1>,<字符串2>字符串1与字符串2不相同则条件满足;不区别大小写对于他们的定义我就不详细例举了,只是在后面的叙述中要用到这些语句。所以先列在这里,如果需要您可以去查更过的资料。4、实例上面对文字性的东西定义了太多了,我们来直接看实例吧,这样比较直观。(本想在这里先引用一个简单的宏汇编的例子的,可是后来又想想算了,如果有人对这篇文章敢兴趣的话,我想简单的宏汇编也应该难不到你。)下面的例子主要是当时在study宏汇编的时候见到网上的例子,在这里就直接引用过来,(虽说是从CSDN上的一个老兄blog里面看到的,但是我感觉是小木偶前辈的例子,所以我一不知道究竟哪位是原作者,您可以看看这里:http://blog.csdn.net/benny5609/archive/2008/06/12/2541246.aspx)范例:通用的推入堆栈宏8086指令的PUSH只能把十六位的寄存器或十六位变量推入堆栈,不能把十六位立即值(常数)或八位的寄存器推入堆栈,而底下这个宏范例,push_op,也能使立即值或八位的寄存器推入堆栈。底下是push_op源代码: page ,132 ;01push_op MACRO arg ;;03reg16 = 0 ;;05reg08 = 0 ;;06addr = 0 ;;07 ;;09检查输入参数是否为16位的寄存器IRP reg,
IFIDN , push arg ;;12如果相等的话,推入堆栈 reg16 = 0ffffh ;;13数定虚拟变量为真 exitm ;;14跳出IRP块 ENDIFENDMIF reg16 ;;17若reg16为真 exitm ;;18则跳出push_op宏ENDIF ;;21检查输入参数是否为16位的寄存器IRP reg, IFIDN , push arg reg16 = 0ffffh exitm ENDIFENDMIF reg16 exitmENDIF ;;33检查输入参数是否为8位的寄存器IRP reg, IFIDN , reg08 = 0ffffh exitm ENDIFENDMIF reg08 IRPC char,arg ;;41取得寄存器名的第一个字母 push char&&x; ;;42推入堆栈 exitm ;;43跳出IRPC块 ENDM exitm ;;45跳出push_op宏ENDIF ;;48检查输入参数是否为8位的寄存器IRP reg, IFIDN , reg08 = 0ffffh ENDIFENDMIF reg08 IRPC char,arg push char&&x; exitm ENDM exitmENDIF ;;62检查输入参数是否为含有寄存器间接寻址模式,即[BX]、[SI]……等等IRPC char,arg IF ('&char;' eq '[') addr=0ffffh exitm ENDIFENDMIF addr push arg exitmENDIF arg_size=((type arg) 1)/2 ;;74输入参数之长度arg_type=(.type arg) and 3 ;;75输入参数之类型 ;;77检查输入参数是否为变量IF arg_type eq 2 arg_offset =0 REPT arg_size arg_address=word ptr arg arg_offset push arg_address arg_offset=arg_offset 2 ENDM exitmENDIF ;;88检查输入参数是否为标号IF arg_type eq 1 push bp mov bp,sp push ax mov ax,offset arg xchg ax,[bp] mov bp,ax pop ax exitm;;98若不是寄存器、寻址模式、变量、标号的话,应为立即值ELSE push bp mov bp,sp push ax mov ax,arg xchg ax,[bp] mov bp,ax pop axENDIFENDM这个宏结构很明显,先检查要推入堆栈的参数是否为16位寄存器(第9行到第31行),如果不是再检查是否为8位寄存器(第33行到第60行),如果不是寄存器的话,再检查是否为寄存器间接寻址(第62行到第72行),如果不是以上这几种的话,再检查是否推入变量到堆栈(第77行到第87行),接下来检查是否推入标号到堆栈(第88行到第97行),假如都不是上述情形的话,就是推入立即值到堆栈了(第98行到第107行)。检查是否为寄存器的方法是用不定重复块( IRP )来指定要比较的范围,故引数列(即第10行角括号内的引数)包含所有16位寄存器名称,但是因为参数与引数都被视为字串,所以大小写是有差别的,必须在引数列里包含不同的大小写排列方式。指定好比较范围后,再用IFIDN比较输入参数是否为引数列中的一个,假如是16位寄存器的话,则直接把该参数推入堆栈即可,并设定一个虚拟变量,reg16,为0ffffh,0ffffh表示真的意思(第12、13行)。然后再跳出push_op堆栈。小木偶再把IRP重复块的执行方式说明一遍。第10行到第16行程序码为:IRP reg, IFIDN , push arg ;;12如果相等的话,推入堆栈 reg16 = 0ffffh ;;13数定虚拟变量为真 exitm ;;14跳出IRP块 ENDIFENDMIF reg16 ;;17若reg16为真 exitm ;;18则跳出push_op宏ENDIF表示在第10行到第16行程序码会重复汇编。第一次时,reg会以AX代入汇编,第11行是比较arg与reg这两字串是否相等,如果相等则汇编第12行到第14行之间的程序码,不相等则结束IFIDN,然后遇到ENDM,故重复第二次,使reg以BX代入汇编……一直到sp所有引数结束。第14行,是因为假如已经找到相符合的寄存器,就没必要再比较了,这样可以加快汇编速度。(虽然也没快多少。)第17行到第19行,也是这样的道理,既已找到是把寄存器推入堆栈,也就没必要汇编以下的程序了,故直接跳出push_op堆栈。您可能会问,第14行就已有了exitm,为何第18行还要有个exitm呢?这是因为RPT、IRP、IRPC这三个重复块,类似宏结构,若要在中间停止汇编都可以用exitm来跳出宏或块,所以第14行是跳出IRP块,第17行是跳出push_op宏。第33行到第61行,是检查参数是否为8位寄存器,方法和上述几乎相同,差别在于8位寄存器(例如ah )无法推入堆栈必须改成16位寄存器(例如ax )。所以第41行到第44行多了个IRPC重复块,此重复块是为了取得寄存器的第一个字母,当取得第一个字母就把该字母加上『x』再推入堆栈,然后跳出IRPC块及push_op宏。至于该IRPC重复块的运作方式如下:该IRPC块重复次数只有两次,分别以8位寄存器名称的两个字母代入汇编,当第一次时即以8位寄存器名称的第一个字母代入,然后加上『x』再推入堆栈,然后立刻跳出IRPC块,故事实上这个重复块只汇编一次而已。第42行的『&&x;』为何要有两个『&』呢?这是因为根据MASM手册上说每层块要使用『&』,故第二层要用两个『&』。第62行到第73行是用来检查推入堆栈的参数是否为寄存器间接寻址,寄存器间接寻址模式是像底下的样子: mov ax,[bx]push [si]sub ax,[bx 200h]观察以上几个例子,您会发现,这种寻址模式含有两个中括号,因此检验方法就是以IRPC检查参数中是否有『[』(第64行到第67行的IF条件汇编),假如有的话,会使虚拟变量,addr,设为真。然后接下来的就直接使该参数推入堆栈,因为PUSH指令就可以直接推入寄存器间接寻址模式。 上面这些东西,其实大多是参考前辈的,希望能给正在寻找宏汇编资料的童鞋一点启发。