参考教程:通俗易懂的汇编语言(王爽老师的书)_哔哩哔哩_bilibili
一、用汇编语言写的源程序
1、用汇编语言编写程序的工作过程
(1)大致流程:程序员编写汇编程序,汇编程序通过编译器转换为计算机能识别的机器码。
(2)汇编程序可认为是包含汇编指令和伪指令的文本。
①伪指令没有对应的机器码的指令,最终不被CPU所执行,它是由编译器来执行的指令,编译器根据伪指令来进行相关的编译工作。
②汇编指令有机器码对应的指令,可以被编译为机器指令,最终被CPU执行。
③汇编程序最后往往有两条固定语句,它们的作用是程序返回,也就是在程序结束运行后,将CPU的控制权交还给使它得以运行的程序。
2、程序中的三种伪指令
(1)段定义:
①一个汇编程序是由多个段组成的,这些段被用来存放代码、数据或当作栈空间来使用。
②一个有意义的汇编程序中至少要有一个段,这个段用来存放代码(也就是代码段)。
③每个段都需要有段名,定义程序中的段可使用以下伪指令。
<段名> segment ;段的开始
;段的内容
<段名> ends ;段的结束
(2)end(并非ends)是汇编程序的结束标记,若程序结尾处不加end,编译器在编译程序时,无法知道程序在何处结束。
(3)assume的中文释义是假设,含义是假设某一段寄存器和程序中的某一个用“segment ... ends”定义的段相关联,例如“assume cs:codesg”指的是CS寄存器与codesg关联,将定义的codesg当作程序的代码段使用。
3、源程序变为机器码的过程
4、汇编程序的结构
(1)一般来讲,汇编程序都是单独编写成源文件后再编译为可执行文件的程序,这个流程适用于编写大型程序。
①汇编程序需要包括汇编指令,还要有指导编译器工作的伪指令。
②源程序由一些段构成,这些段存放代码、数据,或将某个段当作栈空间。
③程序中可用“;”进行注释的添加,使用方法与C语言中的双斜杠一样。
(2)汇编程序的编写举例:写一个程序求出2的3次方。
①定义一个段(代码段)adc。
②实现处理任务(求解2的3次方)。
③指出程序在何处结束。
④将段abc与段寄存器CS关联。
⑤加上程序返回的代码。
5、程序中可能的错误
(1)语法错误:程序在编译时被编译器发现的错误,容易发现。
(2)逻辑错误:程序在编译时不能表现出来的、在运行时发生的错误(往往体现为得到非预期的结果),较难发现。
二、由源程序到程序运行(实践环节须知)
1、由写出源程序到执行可执行文件的过程
2、编辑源程序
编辑源程序可使用的工具非常多,比如记事本、Visual C++等等,初学者推荐使用EditPlus进行编辑。
3、编译
(1)首先将编辑好的源程序文件.asm移动至工作目录,然后打开DOSBox,按照惯例完成工作目录挂接,接着使用masm命令,输入“masm <源程序文件名>”(后面加上“;”可简化操作过程)即可编译源程序。
(2)在编译过程中会产生3个过程文件:
①目标文件(.OBJ)是对一个源程序进行编译要得到的最终结果。
②列表文件(.LST)是编译器将源程序编译为目标文件的过程中产生的中间结果。
③交叉引用文件(.CRF)同列表文件一样,是编译器将源程序编译为目标文件过程中产生的中间结果。
(3)对源程序的编译结束后,编译器输出的最后两行会告诉程序员,这个源程序有没有警告错误(非必须改正,视情况而定)以及必须要改正的错误。
4、连接
(1)在源程序编译好后,再使用link命令,输入“link <源程序文件名>”(后面加上“;”可简化操作过程)即可生成可执行文件。
(2)在连接过程中会产生3个过程文件:
①可执行文件(.EXE)是对一个程序进行连接要得到的最终结果。
②映像文件(.MAP)是连接程序将目标文件连接为可执行文件过程中产生的中间结果。
③库文件(.LIB)里包含了一些可以调用的子程序,如果程序中调用了某一个库文件中的子程序,就需要在连接的时候,将这个库文件和目标文件连接到一起,生成可执行文件。
5、执行可执行程序
(1)在完成连接后,直接输入“<可执行程序文件名>”,即可执行可执行程序,不过屏幕上可能并不会有任何信息显示出来。
(2)DOSBox启动后,计算机由“命令解释器”(程序command.com)控制,运行可执行程序时,command将程序加载入内存,设置CPU的CS:IP指向程序的第一条指令(即程序的入口),使程序得以运行,程序运行结束后,返回到“命令解释器”,CPU继续运行command。
三、用Debug跟踪程序的执行(实践环节须知)
1、用Debug装载程序
(1)首先将编辑好的源程序文件.asm移动至工作目录,然后打开DOSBox,按照惯例完成工作目录挂接,接着输入“debug <可执行程序文件名>”即可完成装载。
(2)举例:如下所示。
①程序加载后,DS中存放着程序所在内存区的段地址,这个内存区的偏移地址为0,则程序所在的内存区的地址为DS:0,这个内存区的前256个字节存放程序段前缀PSP;DOS用来和程序进行通信,从256字节处向后的空间存放的是程序。
②程序加载后,CS的值为DS+10H,CX中存放代码的长度(字节)。
2、用Debug单步执行程序
(1)在装载程序以后,输入“t”即可执行程序中的1条汇编指令,并获取执行后的寄存器结果。
(2)除了输入“t”以外,输入“p”(继续命令P)也会有相类似的结果,不同的是,P命令遇到子程序、中断等时会直接执行,然后输出结果。
(3)输入“g”(运行命令G),Debug会从指定地址处开始运行程序,直到遇到断点或者程序正常结束(G命令还可以指定执行到的代码地址)。
四、方括号和圆括号
1、方括号
(1)在汇编语言中,“[]”中填写的是内存单元的地址(偏移地址),这个地址可以以立即数的形式给出,也可以将地址值存入寄存器中,以寄存器的形式给出。
(2)举例:
指令 | 段地址 | 偏移地址 | 操作单位 |
MOV AX, [0] | 在DS中 | 0 | 字 |
MOV AL, [0] | 在DS中 | 0 | 字节 |
MOV AX, [BX] | 在DS中 | 在BX中 | 字 |
MOV AL, [BX] | 在DS中 | 在BX中 | 字节 |
2、圆括号
(1)在汇编语言中不使用圆括号,它仅仅是方便学习的一种约定,“()”中填写的是内存单元的地址(偏移地址)或者寄存器,整体表示为一个内存单元或寄存器中的内容。
(2)举例:
描述对象 | 描述方法 |
AX中的内容为1145H | (ax) = 1145H |
2000:1000处的内容为4514H | (21000H) = 4514H |
指令MOV AX, [2]的功能 | (ax) = ((ds) * 16 + 2) |
指令MOV [2], AX的功能 | ((ds) * 16 + 2) = (ax) |
指令PUSH AX的功能 | (sp) = (sp) - 2 ((ss) * 16 + (sp)) = (ax) |
指令POP AX的功能 | (ax) = ((ss) * 16 + (sp)) (sp) = (sp) + 2 |
指令ADD AX, BX的功能 | (ax) = (ax) + (bx) |
指令ADD AX, 2的功能 | (ax) = (ax) + 2 |
3、符号idata
(1)在汇编语言中不使用idata,它仅仅是方便学习的一种约定,用于表示常量(或者说立即数)。
(2)举例:
①MOV AX, idata:代表MOV AX, 1、MOV AX, 2……
②MOV AX, [idata]:代表MOV AX, [1]、MOV AX, [2]……
五、LOOP指令
1、LOOP指令使用介绍
(1)LOOP指令的功能是实现算法中的循环结构。
(2)LOOP指令的格式:LOOP <标号>。
(3)使用LOOP指令时的要求:
①CX中要提前存放循环次数,因为(cx)影响着LOOP指令的执行结果。
②要定义一个标号,如同C语言中goto语句需要的标号一样。
(4)CPU执行LOOP指令时要进行的操作:
①(cx) = (cx) - 1。
②判断CX中的值,不为零则转至标号处执行程序,否则向下执行。
2、LOOP指令使用举例
(1)计算2的12次方,结果存储在AX中。
①算法设计:
②汇编程序设计:
assume cs:code
code segment
mov ax, 2
mov cx, 11
s: add ax, ax ;add ax, ax为本程序的循环体
loop s
mov ax, 4c00h
int 21h
code ends
end
(2)计算FFFF:0006字节单元中的数乘以3,结果存储在DX中。
①算法设计:
②汇编程序设计:
assume cs:code
code segment
mov ax, 0ffffh ;在汇编源程序中数据不能以字母开头,要在ffff前面加0
mov dx, ax ;(ds) = (ax) = FFFFH
mov bx, 6 ;(bx) = 6
mov al, [bx] ;(al) = ((ds) * 16 + (bx))
mov ah, 0 ;(ah) = 0
mov dx, 0 ;(dx) = 0
mov cx, 3
s: add dx, ax ;add dx, ax为本程序的循环体
loop s
mov ax, 4c00h
int 21h
code ends
end
六、段前缀的使用
1、段前缀的作用
(1)如下程序乍一看可能没什么问题,用Debug执行它可能也能得到预期的结果,但如果将其经过编译、连接得到可执行程序,那么该程序中的一些代码可能会被编译器“过度优化”,这是程序员不希望发生的。
assume cs:code
code segment
mov ax, 2000h
mov dx, ax
mov al, [0] ;过度优化为mov al, 0
mov bl, [1] ;过度优化为mov bl, 1
mov cl, [2] ;过度优化为mov cl, 2
mov ax, 4c00h
int 21h
code ends
end
(2)为了解决上述问题,可以在“[idata]”前显式地写上段寄存器,如下所示。
assume cs:code
code segment
mov ax, 2000h
mov dx, ax
mov al, ds:[0]
mov bl, ds:[1]
mov cl, ds:[2]
mov ax, 4c00h
int 21h
code ends
end
(3)出现在访问内存单元的指令中,用于显式地指明内存单元的段地址的“ds:”(数据段)、“ss:”(栈段)或“es:”(附加段),在汇编语言中称为段前缀。
2、段前缀的使用举例
(1)目标:将内存FFFF:0000~FFFF:000B中的数据拷贝到0000:0200~0000:020B单元中。
(2)不使用段前缀的方案:
assume cs:code
code segment
mov bx, 0 ;BX中存放偏移地址
mov cx, 12 ;统共拷贝12个(字型)数据
s: mov ax, 0ffffh ;源数据所在位置段地址送至寄存器AX中
mov ds, ax ;将源数据所在位置段地址通过寄存器AX送至寄存器DS中
mov dl, [bx] ;(dl) = ((ds) * 16 + bx)
mov ax,0020h ;拷贝数据目标位置段地址送至寄存器AX中
mov ds, ax ;将拷贝数据目标位置段地址通过寄存器AX送至寄存器DS中
mov [bx], dl ;((ds) * 16 + bx) = (dl)
inc bx ;(bx) = (bx) + 1
loop s
mov ax, 4c00h
int 21h
code ends
end
(3)使用段前缀及附加段寄存器的方案:
assume cs:code
code segment
mov ax, 0ffffh ;源数据所在位置段地址送至寄存器AX中
mov ds, ax ;将源数据所在位置段地址通过寄存器AX送至寄存器DS中
mov ax, 0020h ;拷贝数据目标位置段地址送至寄存器AX中
mov es, ax ;将拷贝数据目标位置段地址通过寄存器AX送至寄存器ES中
mov bx, 0 ;BX中存放偏移地址
mov cx, 12 ;统共拷贝12个(字型)数据
s: mov dl, [bx] ;(dl) = ((ds) * 16 + bx)
mov es:[bx], dl ;((es) * 16 + bx) = (dl)
inc bx ;(bx) = (bx) + 1
loop s
mov ax, 4c00h
int 21h
code ends
end
七、代码段、数据段、栈段的规划
1、在代码段中使用数据
(1)在大型项目中,在程序中直接写地址是非常危险的,因为内存中存放着丰富的内容,如果程序员通过写地址误修改了内存中一些非常重要的数据,这将引发不可预见的后果,为了解决这个问题,可以在程序的段中存放数据,运行时由操作系统分配空间。
(2)程序的段可分为代码段(必须有)、数据段和栈段,各个段中均可以有数据,但需要注意的是,在代码段中定义数据时要用一个标号声明第一条代码的起始位置,否则编译器默认第一条代码的起始位置就是代码段的开头,然而代码段的开头是数据而非代码(如下图所示),将数据当成代码去执行,同样会引发非预期的结果。
(3)定义数据需使用关键字dw、db或dd,其中dw用于定义字型数据,db用于定义一个字节数据,dd用于定义一个双字节数据。
(4)举例:计算8个数据(0123H、0456H、0789H、0abcH、0defH、0fedH、0cbaH、0987H)的和,结果存在AX寄存器中。(第一条代码的起始位置标号为start)
assume cs:code
code segment
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H
start: mov bx, 0 ;偏移地址存放在BX中(代码段第一条代码)
mov ax, 0 ;AX中的内容初始化为0
mov cx, 8 ;统共累加8个双字节数据
s: add ax, cs:[bx] ;(ax) = (ax) + ((cs) * 16 + bx)
add bx, 2
loop s
mov ax, 4c00h
int 21h
code ends
end start
2、在代码段中使用栈
(1)在代码段中使用栈,首先要为栈预留足够的空间,这可以通过在代码段中定义数据实现(定义一些无用数据,它们可以起到占位的作用),然后明确栈顶指针指向的位置(段地址+偏移地址),将其送入段寄存器SS中,这样就完成了在代码段中定义栈空间的过程。
(2)举例:利用栈结构将上例中的8个数据逆序存放。(第一条代码的起始位置标号为start)
assume cs:code
code segment
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H
dw 0, 0, 0, 0, 0, 0, 0, 0
start: mov ax, cs
mov ss, ax ;代码段的段地址作为栈段的段地址,存放在SS中
mov sp, 30h ;栈顶指针的偏移地址存放在SP中
mov bx, 0 ;偏移地址存放在BX中
mov cx, 8 ;统共8个数据入栈
r: push cs:[bx] ;(sp) = (sp) - 2 ((ss) * 16 + (sp)) = ((cs) * 16 + (bx))
add bx, 2
loop r
mov bx, 0 ;偏移地址存放在BX中
mov cx, 8 ;统共8个数据出栈
c: pop cs:[bx] ;((cs) * 16 + (bx)) = ((ss) * 16 + (sp)) (sp) = (sp) + 2
add bx, 2
loop c
mov ax, 4c00h
int 21h
code ends
end start
3、将数据、代码、栈放入不同段
(1)上例的程序中,数据、栈和代码都在一个段,程序显得混乱,编程和阅读时都要注意何处是数据、何处是栈、何处是代码,为了避免这种麻烦,建议数据、栈和代码各自放在不同的段中,如下图所示。
(2)更改上例程序,在功能不变的前提下将数据、代码、栈放入不同段。
assume cs:code, ds:data, ss:stack
data segment ;数据段
dw 0123H, 0456H, 0789H, 0abcH, 0defH, 0fedH, 0cbaH, 0987H
data ends
stack segment ;栈段
dw 0, 0, 0, 0, 0, 0, 0, 0
stack ends
code segment ;代码段
start:
;初始化各段寄存器(代码段寄存器不需要初始化)
mov ax, stack
mov ss, ax ;栈段的段地址存放在SS中
mov sp, 20h ;栈顶指针的偏移地址存放在SP中(这决定了栈的大小)
mov ax, data
mov ds, ax ;数据段的段地址存放在DS中
;入栈
mov bx, 0 ;偏移地址存放在BX中
mov cx, 8 ;统共8个数据入栈
r: push [bx] ;默认为代码段前缀,可不加段前缀
add bx, 2
loop r
;出栈
mov bx, 0 ;偏移地址存放在BX中
mov cx, 8 ;统共8个数据出栈
c: pop [bx] ;默认为代码段前缀,可不加段前缀
add bx, 2
loop c
mov ax, 4c00h
int 21h
code ends
end start
八、处理字符问题
1、字符在汇编程序中的表示
(1)汇编程序中,用’......’的方式指明数据是以字符的形式给出的(一个字符占一个字节的空间),编译器将把它们转化为相对应的ASCII码。
(2)以字符的形式定义数据举例:
assume cs:code, ds:data
data segment
db ‘unIX’
db ‘foRK’
data ends
code segment
start: mov al, ‘a’ ;字符’a’对应的ASCII码送入寄存器AL中
mov bl, ‘b’ ;字符’b’对应的ASCII码送入寄存器BL中
mov ax, 4c00h
int 21h
code ends
end start
2、大小写字母问题
(1)大写的26个字母的ASCII码是紧挨着的,小写的26个字母的ASCII码也是紧挨着的,并且一个字母的大写和小写,其对应的ASCII码存在这样的关系——小写字母的ASCII码值比大写字母的ASCII码值大20H(并且它们的二进制形式只有第6位有区别,大写字母为0,小写字母为1,这为大小写字母转换提供了另一种思路)。
大写字母 | 二进制形式 | 小写字母 | 二进制形式 |
A | 01000001 | a | 01100001 |
B | 01000010 | b | 01100010 |
C | 01000011 | c | 01100011 |
D | 01000100 | d | 01100100 |
(2)大小写字母转换举例:对数据段定义的两个字符串,将第一个字符串中的小写字母转换为大写字母,将第二个字符串中的大写字母转换为小写字母。
assume cs:codesg, ds:datasg
datasg segment
db ‘BaSiC’
db ‘iNfOrMaTiOn’
datasg ends
codesg segment
start: mov ax, datasg
mov ds, ax ;初始化段寄存器DS,它要存放数据段的段地址
;将第一个字符串‘BaSiC’中的小写字母转换为大写字母
mov bx, 0 ;存放数据段偏移地址
mov cx, 5
s1: mov al, [bx] ;将字符串中的字符依次送入al,每轮循环送入一个
and al 11011111b ;将al中的第6位二进制数置0
mov [bx], al ;将大写转换后的字符送回它原来的位置
inc bx ;(bx) = (bx) + 1
loop s1
;将第二个字符串‘iNfOrMaTiOn’中的大写字母转换为小写字母
mov bx, 5 ;存放数据段偏移地址
mov cx, 11
s2: mov al, [bx] ;将字符串中的字符依次送入al,每轮循环送入一个
or al 00100000b ;将al中的第6位二进制数置1
mov [bx], al ;将小写转换后的字符送回它原来的位置
inc bx ;(bx) = (bx) + 1
loop s2
mov ax, 4c00h
int 21h
codesg ends
end start