8086 masm6.11(1981,1988)宏魔法(dosbox下)

Thursday, June 8, 2023
本文共2299字
5分钟阅读时长

⚠️本文是作者P3troL1er原创,首发于https://peterliuzhi.top/posts/8086-masm6.1119811988%E5%AE%8F%E9%AD%94%E6%B3%95dosbox%E4%B8%8B/。商业转载请联系作者获得授权,非商业转载请注明出处!

People usually compare the computer to the head of the human being. I would say that hardware is the bone of the head, the skull. The semiconductor is the brain within the head. The software is the wisdom. And data is the knowledge. — Masayoshi Son

引入

最近在写汇编语言大作业,因为老师讲了一些汇编宏的用法,让我大为新奇,于是在大作业里面普遍用到了宏,在这里记录一下

另外,教学中用的masm好像比较老旧,很多语法都支持不了(是masm自己的原因吗,好像不支持leave指令,不过我用宏自己定义了一个),很多网上搜到的高级宏用法都用不了,所以这里分享的内容其实离我的想象还有一段距离,也算是一种遗憾吧

宏的基本知识

无参数宏

类似于C语言中的宏常量的真子集(不包括宏定义的常量数字等),如下定义:

<宏名> macro
	...
endm

会直接把宏名替换为里面的内容,由此可以实现leave指令:

leave macro
	mov sp, bp
	pop bp
endm

同时也可以定义一下进入函数时的准备工作:

intofunc macro
	push bp
	mov bp, sp
endm

有参数宏

类似于C语言中的宏函数:

<宏名> macro <参数1>, <参数2>, <参数3>, ...
	...
endm

类似于C语言中:

#define <宏名>(<参数1>, <参数2>, <参数3>, ...) ...

不过在我所使用的这个版本的masm中好像不支持变参宏

<>操作符

把尖括号内的看作一个整体把一个完整的实参括起来,成为一个新的实参,比如:

mymacro <offset mydata>

其中把offset mydata作为一个整体传给了mymacro

在C语言中没有对应的操作符,硬说只能是括号

&操作符

表示拼接,意同C语言中的##,例如:

shift_var marco r_m,direct,count
	  mov cl,count
	  s&direct r_m,cl
  endm

其中把s和direct所代表的文本拼接在了一起

%操作符

告诉宏汇编程序获取表达式的值,而不是获取表达式文本本身,一般用于给宏传递实参(宏调用),但有些情况不能用,要确保%后面是一个编译期表达式

!操作符、;;注解符

!表示后面的操作符不是操作符而是普通的字符

;;表示宏中的注解,宏不会把这段注解展开(只使用;,预处理后的代码中会多出注解)

文本替换宏

在新版的masm中好像有TEXTEQU指令,但是在我的masm中只有EQU指令

它可以实现C语言中的宏常量变量,也可以简单地用于替换一段文本

call_print EQU <func1 print>
STACK_SIZE EQU 1024

local伪指令

在宏定义中使用变量名和标号,为了避免在宏展开时产生多个相同的变量名或标号(比如有时候会在宏中定义循环和跳转)

宏展开时,local伪指令指定的变量、标号自动生成格式为??ⅩⅩⅩⅩ的符号,其中后四位顺序使用0000~FFFF的十六进制数字。

delay macro reg  ;; reg为16位寄存器
	local lop
	mov cx,reg
	lop: nop
	     loop lop
endm

宏嵌套

可以使用一个宏生成另一个宏的定义,这样就可以定义一个生成器,然后通过给这个宏传不同的参数,从而批量生成宏

重复汇编

可以使用rept指令来重复生成一段汇编,将重复语句序列重复汇编,表达式的值为重复汇编的次数。也可以重复生成一段宏。

rept 表达式
	...
endm

【例】用重复汇编可以在9*9个字节存储单元中存放一个乘法九九表的数值:

n=0
rept 9
	n=n+1
	m=0
	rept 9
	   m=m+1
	   db n*m
	endm
endm 

irp指令将重复语句序列重复汇编,次数由实参个数所决定

irp 形参,<实参1,实参2,……>
	...
endm
pushreg macro text
	irp reg,<text>
		push reg
	endm
endm

irpc指令将重复语句序列重复汇编,次数由字符串中字符个数决定

irpc 形参,字符串
	...
endm 

条件汇编

宏汇编提供了十种条件宏汇编伪操作指令:if、ife、if1、if2、ifdef、ifndef、ifb、ifnb、ifidn、ifdif

if: 当表达式的值不为零,则条件为真;
ife:如果表达式的值为零,则条件为真;
if1: 如果是第一遍扫描,则条件为真;
if2:如果是第二遍扫描,则条件为真;
ifdef:表达式为符号。如果该符号已经定义,则条件为真。
ifndef:表达式为符号。如果符号没有定义,则条件为真。
ifb <count>:如果尖括号中为空,则条件为真。
ifnb <count>:如果尖括号中不为空,则条件为真。
ifidn <char1>, <char2>:如果两个字符串相同则条件为真
ifidf <char1>, <char2>:如果两个字符串不同则条件为真。

使用语法:

ifx ×× 表达式
	...
else
	...
endif

类似于C语言中的#if#endif#ifdef#ifndef

宏魔法实例

x86风格函数传参

pusharg macro val
    mov ax, val
    push ax
endm

类C函数原型声明

func0 macro funcname
    call funcname
endm

func1 macro funcname, v1
    mov ax, v1
    push ax
    ; pusharg v1
    call funcname
    add sp, 2
endm

func2 macro funcname, v1, v2
    mov ax, v2
    push ax
    mov ax, v1
    push ax
    call funcname
    add sp, 4
endm

func3 macro funcname, v1, v2, v3
    mov ax, v3
    push ax
    mov ax, v2
    push ax
    mov ax, v1
    push ax
    call funcname
    add sp, 6
endm

call_print EQU <func1 print>
call_puts EQU <func1 puts>
call_putchar EQU <func1 putchar>
call_read EQU <func3 read>
call_strlen EQU <func1 strlen>
call_strcmp EQU <func2 strcmp>
call_itoa EQU <func2 itoa>
call_atoi EQU <func1 atoi>
call_getchar EQU <func0 getchar>
call_getint EQU <func0 getint>
call_memset EQU <func3 memset>
call_memcpy EQU <func3 memcpy>
call_swap EQU <func2 swap>

call_gets macro v1, v2, v3
    mov ax, v3
    push ax
    mov ax, v2
    push ax
    mov ax, v1
    push ax
    call read
    add sp, 6
    mov byte ptr ds:[di], 0
endm

使用的时候的示例:

call_print <offset change_index_st>
call_gets <offset search_key_st>, <offset buffer>, STU_NAME_SIZE

在函数中取形参

这里我使用的是x86风格传参,因此参数都保存在栈中

arg macro i
    arg&i macro reg
        mov reg, [bp+2*(i+1)]
    endm
endm

; 声明arg1~arg7
n=0
rept 7
    n=n+1
    arg %n
endm

使用的时候,比如read有三个参数:

; read(read_instruction, read_storage, read_size)
read:
    intofunc
    arg1 ax
	...
    arg2 di
	...
    arg3 cx
    ...
    leave
    ret

而且只要不更改bp下面(地址上是上面)的栈,在函数任意位置都能取参

进入函数与退出函数

上面已经说过了

intofunc macro
    push bp
    mov bp, sp
endm

leave macro
    mov sp, bp
    pop bp
endm

初始化程序与结束程序

这个比较简单,就普通的替换

prog_initailize macro
    mov ax, data
    mov ds, ax
    mov ax, 0B800H
    mov es, ax
    ;; mov ax, stack
    ;; mov ss, ax
    ;; mov sp, STACK_SIZE
    mov bp, sp
    xor di, di
    xor si, si
    xor ax, ax
    xor bx, bx
    xor cx, cx
    xor dx, dx
endm

prog_doom macro
    mov ax, 4c00h
    int 21h
endm

在全局变量保存寄存器

有时候我们不想用复杂的栈操作,在保证函数内嵌套调用的函数不会破坏全局变量的前提下,可以在全局变量暂存寄存器。对每个寄存器都有一个暂时的存储空间:

save macro reg
    mov storage_&reg, reg
endm

load macro reg
    mov reg, storage_&reg
endm

然后在数据段声明:

storage_di dw 0
storage_si dw 0
storage_bx dw 0
storage_ax dw 0
storage_cx dw 0
storage_dx dw 0

只要数据段的相应标号存在,save和load就能起效

宏常量声明

很简单的类似#define的声明

STACK_SIZE EQU 1024
STU_NAME_SIZE EQU 20h
STU_NUMBER_SIZE EQU 15
STU_COUNT EQU 10
CONTINUE_COUNT EQU 3
SCORE_COUNT EQU 2