本页目录

【汇编语言】汇编语言编程

本章将开始介绍汇编语言编程。与上一章【汇编语言】访问寄存器和内存不同的是,上一章的汇编代码是直接在DOSBox终端中,通过debug交互输入的,数据默认为16进制数;而本章将使用文本编辑器编写汇编代码,然后通过汇编器生成可执行文件,数据默认为10进制数,在编程时需要结尾以h标明,请注意区分!

基本结构示例

assume cs:codesg
codesg segment
           mov ax,0123h
           mov bx,0456h
           add ax,bx
           add ax,ax

    ;程序返回的套路
           mov ax,4c00h
           int 21h
codesg ends
end

伪指令

上面的示例代码高亮部分为伪指令,剩下的主体部分为汇编指令。伪指令没有对应的机器指令,最终不被CPU执行,而是由汇编器处理的指令。上面的示例代码出现了三种伪指令:

段定义

一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。一个有意义的汇编程序至少要有一个用来存放代码的段。每个段都需要有段名,段定义的格式如下:

segname segment
    ;...
segname ends

end

汇编程序的结束标记。

assume

assume伪指令用来指定段寄存器和段名之间的关系。在这个例子中,assume cs:codesg表示CS寄存器与codesg段相关联,将定义的codesg当作程序的代码段使用。

运行一个汇编程序

写好一个汇编程序(文本文件)1.asm后,在终端执行:

masm 1.asm;

其中的分号表示使用默认文件名,此操作会得到1.obj文件;然后执行:

link 1.obj;

分号含义同上,此操作会得到1.exe,如果想在终端直接运行,输入1.exe即可:

1.exe

如果想借助debug跟踪程序的执行,则输入命令:

debug 1.exe
img

一些符号约定

[...]和(...)

[...]是汇编语法,表示一个内存单元,段地址在DS中,偏移地址由...给出;
(...)是为了学习方便做出的约定,表示一个内存单元或寄存器中的内容。

例如,pop ax的功能可以描述为:

(ax)=((ss)*16+(sp))

(sp)=(sp)+2

idata

用符号idata表示常量。(也就是立即数,immediate data

loop指令

assume cs:codesg
codesg segment
           mov  ax,2
           mov  cx,7
    s:     add  ax,ax
           loop s

           mov  ax,4c00h
           int  21h
codesg ends
end

loop指令功能是实现计数型循环,会默认使用CX寄存器的值作为循环计数器,当执行loop指令时会进行操作:

(CX)=(CX)-1

判断(CX)是否为0,如果不为0,则跳转到标号处继续执行循环体;如果为0,则继续执行下一条指令。

inc指令

inc ax  ;(ax)=(ax)+1

inc指令的功能是自增。

段前缀

debug中使用-a编写汇编代码访问内存时,可以直接使用mov ax,[idata],但在汇编程序中,需要使用段前缀,写为mov ax,ds:[idata]。例如在汇编程序中,mov ax,[7]等同于mov ax,7,如果是想访问ds:7则需要改写为mov ax,ds:[7]

汇编程序也可以间接访问内存,例如mov ax,[bx]是没问题的,同时和mov ax,ds:[bx]也是等价的。

mov ax,[7]     ;(ax)=7
mov ax,ds:[7]  ;(ax)=((ds)*16+7)

mov ax,[bx]    ;(ax)=((ds)*16+(bx))
mov ax,ds:[bx] ;(ax)=((ds)*16+(bx))

在代码段中使用数据

此前的演示中,在汇编程序中直接访问物理地址其实是危险的,因为原处可能有其他的重要数据。规范的做法是在程序的段中存放数据,运行时会由操作系统分配空间。

assume cs:codesg
codesg segment
           dw   3412h,7856h,0ab90h,0efcdh,0,0,0,0
           dw   0,0,0,0,0,0,0,0
           dw   0,0,0,0,0,0,0,0

           mov  ax,cs
           mov  ss,ax
           mov  sp,30h

    ;入栈
           mov  bx,0
           mov  cx,4
    s1:    push cs:[bx]
           add  bx,2
           loop s1

    ;出栈
           mov  bx,10h
           mov  cx,4
    s2:    pop  cs:[bx]
           add  bx,2
           loop s2

           mov  ax,4c00h
           int  21h
codesg ends
end

上面的代码希望将数据通过栈倒序存放。使用dw(define word)关键字定义了一片数据空间,类似的操作还有:

db:定义一个字节的数据

dw:定义一个字的数据

dd:定义一个双字的数据

img

通过-u指令反汇编发现CPU将dw定义的数据当成了指令,通过-d命令能够清楚的看到指令应该从076a:0030处开始。因此还需要定义一个标号,指示代码开始的位置:

assume cs:codesg
codesg segment
           dw   3412h,7856h,0ab90h,0efcdh,0,0,0,0
           dw   0,0,0,0,0,0,0,0
           dw   0,0,0,0,0,0,0,0

    start:
           mov  ax,cs
           mov  ss,ax
           mov  sp,30h

    ;入栈
           ;...

    ;出栈
           ;...

           mov  ax,4c00h
           int  21h
codesg ends
end start
img

可以看到IP的初值是30,是正确的指令开始的位置;结果也正确的保存在076a:0010开始的空间中。

将数据、代码、栈放入不同段

下面是一种实用的程序结构,将数据、代码、栈放入不同段中,仍然使用上一节的例子:

assume cs:codesg,ds:datasg,ss:stcksg

datasg segment
           dw 3412h,7856h,0ab90h,0efcdh,0,0,0,0
           dw 0,0,0,0,0,0,0,0
datasg ends

stcksg segment
           dw 0,0,0,0,0,0,0,0
stcksg ends

codesg segment
    start:
    ;初始化寄存器
    ;由于程序至少要有一个代码段,所以会自动给CS赋值
    ;此处不需要手动初始化CS
           mov  ax,datasg
           mov  ds,ax
           mov  ax,stcksg
           mov  ss,ax
           mov  sp,10h

    ;入栈
           mov  bx,0
           mov  cx,4
    s1:    push ds:[bx]
           add  bx,2
           loop s1

    ;出栈
           mov  bx,10h
           mov  cx,4
    s2:    pop  ds:[bx]
           add  bx,2
           loop s2

           mov  ax,4c00h
           int  21h
codesg ends
end start

注意现在入栈、出栈操作时段地址寄存器是DS

img

练习

loop指令实现乘法

编程计算ffff:6字节单元的数值乘以3(连加三次),结果保存在DX中。

assume cs:codesg
codesg segment
           mov  bx,0ffffh
           mov  ds,bx
           mov  bx,6
           mov  ah,0
           mov  al,[bx]

           mov  dx,0
           mov  cx,3
    s:     add  dx,ax
           loop s

           mov  ax,4c00h
           int  21h
codesg ends
end
imgimg

注:在汇编程序中数据不能以字母开头,因此ffffh要写为0ffffh

计算连续内存单元之和

编程计算ffff:0~ffff:b字节单元的数据之和,结果保存在DX中。

注意我们要计算的字节单元(8位),每个单元最大为255,理论上总和一定不会超过DX的上限值(16位),但单次相加时要注意需要取出8位的数据,同时加到16位的寄存器,以保证结果正确且单次相加不会溢出。

assume cs:codesg
codesg segment
           mov  bx,0ffffh
           mov  ds,bx

           mov  bx,0         ;第i个数
           mov  dx,0         ;总和
           mov  cx,0ch       ;注意边界,(cx)=0bh+1=0ch
    s:     mov  ah,0
           mov  al,[bx]      ;取出8位数据
           add  dx,ax        ;计算16位数据
           inc  bx
           loop s

           mov  ax,4c00h
           int  21h
codesg ends
end

结果是405h

img

复制内存(使用loop和段前缀)

编程实现将ffff:0~ffff:b的数据复制到0:200~0:20b

在本题中需要两个段的数据,默认的段前缀是DS,我们再使用附加段寄存器ES,使得(ds)=ffffh(es)=20h,这样就可以对齐两个段的数据(ds:[bx]直接对应es:[bx]),可以使得程序更简明。

assume cs:codesg
codesg segment
           mov  bx,0ffffh
           mov  ds,bx
           mov  ax,20h
           mov  es,ax         ;使用附加段寄存器

           mov  bx,0          ;第i个数
           mov  cx,0ch        ;注意边界,(cx)=0bh+1=0ch
    s:     mov  al,[bx]       ;默认DS为段地址
           mov  es:[bx],al    ;这里使用ES做段前缀
           inc  bx
           loop s

           mov  ax,4c00h
           int  21h
codesg ends
end
img