汇编语言
零. 学习介绍和使用工具
【1】我们使用的教材是机械工业出版社的《32 位汇编语言程序设计第二版》。
指导老师是福州大学的倪一涛老师。
这门课程教授的是 Intel 80*86 系列处理器的 32 位汇编。我们现在的处理器都兼容这个处理器。
这篇博客只是大二下汇编语言学习的总结,用于基础入门。
【2】环境和工具:
电脑系统:Windows11
VC6 和 Devc++: 用于看 c 语言对应的汇编,便于研究(也可以直接使用里面的 g++)
汇编器:masm6.15– 编译汇编,将汇编程序执行
调试器:ollydbg
工具的使用这里省略。
【3】汇编指令的执行过程:
一. 从机器语言到汇编语言
程序设计语言是人与计算机沟通的语言,程序员利用它进行软件开发。通常人们习惯使用类似自然语言的高级程序设计语言,如 C,C++,Basic,Java 等。
高级语言需要翻译为计算机能够识别的指令 (机器语言),才能被计算机执行。机器语言是一串 0 和 1 组成的二进制代码,如果直接用它来编写程序太过于晦涩难懂且开发效率极低,称为低级语言。
于是我们将二进制的指令和数据用便于记忆的符号(助记符,Mnemonic)表示就形成汇编语言(Assembly), 所以汇编语言是一种面向机器的低级程序设计语言,也称为低层语言。
二. 寄存器与存储
【1】存储有分为外存储器和内存储器。外存储器就是磁盘。而内存储器就是寄存器,内存(分为主存和缓存)。这里重点介绍一下寄存器。
【2】寄存器分类
(1)通用寄存器(重点)
32 为兼容 16 位和 8 位。所以我们寄存器写 EAX 或 AX 都可以。EAX 和 AX 可以分为 AH 和 AL 也就是高低两个寄存器。其他也是这样。
地址指针寄存器尽量不要用,很容易导致程序出错。
数据寄存器的习惯用法:
(2)段寄存器和专用寄存器(一般在系统内核用到,我们用不到,了解即可)
总结:一般使用 EAX、ECX、EDX、EBX、ESP、EBP、ESI、EDI。
其中 ESP 和 EBP 一般用来存储函数栈指针,一般不会用来存数据,一般在函数那块用到。
实际上用来存数据的是 EAX、ECX、EDX、EBX、ESI、EDI 这六种
三. 汇编指令
0. 概述
汇编语言由以下 3 类组成:
- 汇编指令(机器码的助记符)
- 伪指令(由编译器执行)
- 其它符号(由编译器识别)
汇编语言的核心是汇编指令,它决定了汇编语言的特性。
具体语法:
1. 注释
汇编语言分号后面是注释。
2. 基本框架
例:
1 |
|
四. 数据的定义
在汇编语言中,不同的数据类型用来表示不同长度的数据。常见的数据类型,如:
BYTE:一个字节(8 位)
WORD: 一个字(16 位)
DWORD: 双字(32 位)【也可以写成 dd】
QWORD:四字(64 位)
注:一个字等于两个字节!寄存器是双字。
1 |
|
如果数值不确定,可以用? 代替值
1 |
|
五. 通用数据处理指令
1.mov 传送指令
【1】传送指令 mov
传送指令 MOV 把一个字节,字或双字的操作数从源位置传送至目的位置,可以实现常数,通用寄存器,主存(内存)之间的数据的传送。
1 |
|
注意点:(1)指令中两操作数中至少有一个为寄存器,不能都是常数或内存。
(2)EIP 不能作目的寄存器。
(3)mov 操作时源操作数与目的操作数数据长度要一致。不一致要用下面的 movsx 或 movzx。
【2】MOVSX 带符号扩展传送指令
例: MOVSX EAX,CL
把 CL 寄存器中的 8 位数,符号扩展成 32 位数, 送到 EDX 寄存器。
MOVSX EDX,word ptr[EDI]
把 DS 段中由 EDI 指定地址的 16 位数符号扩展成 32 位数,送到 EDX 寄存器。
【3】MOVZX 带零扩展传送指令
例: MOVZX DX,AL
把 AL 寄存器中的 8 位数,零扩展成 16 位数,送 到 DX 寄存器。
MOVZX EDX,DATA
把 DATA 单元的 16 位数零扩展成 32 位数,送到 EDX 寄存器。
有符号扩展和无符号拓展的差别:
比如内存中的一个八位数 1010 0011
有符号拓展成 16 位就是把最高一位当成符号位拓展:1000 0000 0010 0011无符号拓展成 16 位就是把所有位都当成数值位拓展:0000 0000 1010 0011
【4】lea 传送变量地址到寄存器
1 |
|
2. 堆栈操作指令
【1】PUSH 指令
push 指令用于将数据压入栈中。语法如下:
1 |
|
其中 operand 可以是寄存器、内存地址或立即数。
(1)如果 operand 是寄存器,则该寄存器中的内容将被压入栈中。
(2)如果 operand 是内存地址,则将该地址处的数据压入栈中。
(3)如果 operand 是立即数,则将该立即数值压入栈中。
1 |
|
【2】POP 指令
pop 指令用于从栈中弹出数据。语法如下:
1 |
|
其中 operand 可以是寄存器或内存地址。
(1)如果 operand 是寄存器,则栈顶元素将被弹出并存入该寄存器。
(2)如果 operand 是内存地址,则栈顶元素将被弹出并存入该地址所指向的内存单元。
1 |
|
注意事项:push 和 pop 操作是成对出现的,每个 push 都应该有一个对应的 pop。
【3】PUSHAD 和 POPAD
1.PUSHAD
在 32 位环境下,PUSHAD 指令将通用寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 的值依次压入栈中。语法如下:
1 |
|
执行 PUSHAD 指令后,栈顶元素为 EDI 寄存器的值,依次向下是 ESI、EBP、ESP、EBX、EDX、ECX、EAX。
2.POPAD
在 32 位环境下,POPAD 指令将栈顶的数据弹出并存入通用寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 中。语法如下:
1 |
|
执行 POPAD 指令后,栈顶的数据将先弹出到 EDI 寄存器,然后依次向下弹出到 ESI、EBP、ESP、EBX、EDX、ECX、EAX。
3. 算术运算指令
【1】加减乘除 –add,sub,mul,div 等
(1)add destination, source
(2)sub destination, source
注:destination– 目标操作数 source– 源操作数
上面指令的两个参数也一样不能都是常数或内存。
(3)mul 目标操作数
目标操作数可以是寄存器或内存(直接或间接寻址)。
使用 MUL 指令时,需要注意以下几点:
MUL 指令只能用于无符号整数乘法。乘法操作的结果存储在多个寄存器中,具体取决于操作数的大小。
1.8 位: 被操作数存放在 al 中,则结果存储在 AX 寄存器中,高 8 位部分存储在 AH 寄存器中。
2.16 位:被操作数存放在 ax 中**,** 结果高 16 位部分存储在 DX 寄存器中,低 16 位部分存储在 AX 寄存器中。
3.32 位:被操作数存放在 eax 中,结果高 32 位部分存储在 EDX 寄存器中,低 32 位部分存储在 EAX 寄存器中。
以下是一些示例:
1 |
|
(4)div 目标操作数
除数是 16 位寄存器: 被除数是 16 位寄存器 DX 与 16 位寄存器 AX
组成: (DX)*2 16 +(AX) 指令执行完 商存放在 AX 余数存放在 DX
除数是 32 位寄存器: 被除数是 32 位寄存器 EDX 与 32 位寄存器 AX
组成: (EDX)*2 32 +(EAX) 指令执行完 商存放在 EAX 余数存放在 EDX
1 |
|
注意一定要将后面存余数的 edx 寄存器初始化,不然除法运算会出错。
【2】某个值 ++ 或 –
++ inc inc eax
– dec dec eax
注:如果数据存在栈中,不能直接 inc [ebp-8],而要指定内存单元的大小。
如 –inc dword ptr [ebp-8]
4. 其他运算符
除了上面的指令,还有以下类型的指令:
(1)逻辑运算指令 – 实现与或非和异或,测试等逻辑运算
(2)移位指令
(3)串操作指令
(4)程序控制指令
(5)处理器控制指令
六. 函数初步 – 函数的使用
1. 汇编中也有函数,将一系列操作进行封装。
函数调用指令:Call f
2. 输入输出
注:本课程的输出输出函数在 Irvine32 库中,该函数库中函数大全及其详细解释在
\asm\Examples\Lib32\Irvine32.asm 这个文件中。
不要去 \ asm\INCLUDE 这个目录里面找,那里面那个文件没有函数解释。
下面举几个常用到的函数。
(1)输入输出整数 –ReadInt 和 WriteInt
格式:
无符号输出:WriteDec
注:这里我们使用函数之前,要将输出的目标整数先存入 eax 中。另外这里我们输入输出使用的函数是 Irvine32 库,它提供 32 位输入输出功能。
(2)输出字符串 –WriteString,输出 edx 中存储内存地址指向的变量
案例:打印 hello world
1 |
|
注:.data 部分用于声明和初始化数据段(存储在内存中的变量)。
在这里,hello_msg db “Hello World!”, 0 这行代码的含义是:
(1)hello_msg:变量名称。
(2)byte:byte 是一个伪指令,表示内存开辟一个字节的空间存储变量。
(3)”Hello World!”:这是要存储的字符串数据,即 “Hello World!”。
(4)0:这里的 , 0 表示以 0 结尾,用于表示字符串的结束。在汇编语言中,定义字符串要在后面自己加上 0 当作终止符。(5)offset:offset 是来获取内存地址的。因为 writeString 需要的是变量地址,而不是变量,所以这里字符串不能直接 mov edx, hello_msg。而应该使用 offset 关键字来获取变量的地址放入 edx 中,这里存的是地址而不是值。
【当然 mov edx, hello_msg 也不会成功,因为字符串 “hello world” 是 byte 类型,只有 4 位,而 edx 是 8 位,所以 mov 要用带符号位扩展 movzx】
这行代码的作用是定义了一个以 “Hello World!” 结尾的字符串并将其存储在内存中,然后将其地址放在 edx 中,然后调用 WriteString 函数去内存中找到地址对应的字符串将其输出。
(3)输出字符 –WriteChar,输出 al 中的字符
1 |
|
(4)输出换行
call crlf ; 输出换行
七. 定义数组
1. 定义数组
1 |
|
如果要数组一次性赋值:
1 |
|
2. 访问数组
访问第一个元素
1 |
|
注:writeint 的本质是读取 eax 的四个字节,然后将这四个字节的数据转化为 int 值。
mov eax, a 是将第一个元素的值放入 eax 中。
数组第 i 个元素访问:
1 |
|
还有一种 a+i4 更加好看更接近 c++ 的写法就是 a[i4],一般我们都这样写。
注:[]这个加不加知识规范的问题,a+i4 和 [a+i4] 实际上和 a[i*4]是一致的。
这里加减的单位是字节。
1 |
|
3.[]– 间接寻址
【1】如果 [] 里面是寄存器,则会根据寄存器中存储的内存地址取得对应的值。
1 |
|
【2】如果 [] 里面是内存变量(不管是全局还是栈中)或立即数,则和没加一样。
比如下面这两个指令的地址是一样的。
1 |
|
八. 分支和循环结构的汇编实现
1. 基础 – 比较和跳转指令
一般使用比较和跳转实现分支结构
比较指令: cmp x,y
跳转指令: jmp, jXXX(ja, jb, jz)
cmp x, y:执行操作 x-y (x 与 y 的值不变), 根据操作结果改变 EFLAG 相应的位。
jmp 是无条件跳转,而 jXXX 是有条件跳转。
比如
ja loc: 若 x 与 y 是无符号数 (程序员定义) 且 x>y, 则程序跳转到地址 loc 处执行。
jz/je loc: 若 x 与 y 是无符号数 (程序员定义) 且 x==y, 则程序跳转到地址 loc 处执行
jb loc: 若 x 与 y 是无符号数 (程序员定义) 且 x<y,则程序跳转到地址 loc 处执行
jg loc: 若 x 与 y 是有符号数 (程序员定义) 且 x>y, 则程序跳转到地址 loc 处执行
jz/je loc: 若 x 与 y 是符号数 (程序员定义) 且 x==y, 则程序跳转到地址 loc 处执行
jl loc: 若 x 与 y 是无符号数 (程序员定义) 且 x<y,则程序跳转到地址 loc 处执行jge loc 有符号数 x>=y
jle loc 有符号树 x<=y
2. 分支结构汇编实现 – 实际是比较和跳转
分支具体思路:
1 |
|
最终模板:
(1)if-else
1 |
|
(2)if
1 |
|
分支结构案例:
求整数 a 与 b 最大值, 并在屏幕中输出最大值
算法设计:
1
2
3
4
if a > b then
max=a
else
max=b代码实现题目:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
include irvine32.inc ; 包含 Irvine32 库
.data
a DWORD 0x10 ;DWORD,双字32位,0x10是16进制的10
b DWORD 0x20
.code
main PROC
mov eax, a
cmp eax, b
jna maxb
jmp final
else:
mov eax, b
final :
call writeint
exit
main ENDP
END main
3. 循环结构汇编实现 – 实际是判断
循环结构具体思路:
1 |
|
循环模板:
1 |
|
循环结构案例:
在内存中存有 10 个整数,求这 10 整数 最大值, 并在屏幕中输出最大值
题目算法:
1
2
3
4
5
6
7
8
9
10
11
int eax = arr[0]; // eax存放最大值
int esi = 0; // esi存放数组元素下标
while (esi < 10) {
if (eax < arr[esi]) {
eax = arr[esi];
}
esi++;
}
writeint(eax);代码实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
main proc
mov eax, arr[0];eax存放最大值
mov esi, 0; esi存放数组元素下标
startFor:
cmp esi, 10
jge endFor
cmp eax, arr[esi*4]
jge L1
mov eax,arr[esi*4];
L1:
add esi, 1;
jmp startFor
endFor:
call writeint
exit
main endp
九. 函数详解 – 自定义函数
- 用于模块化、是重要的封装机制
- 函数定义方式与执行逻辑
- 参数传递方法:
【1】内存变量 (数据段) 方式 – 简单说就是在. data 下定义全局变量,所有函数都可以用。一般定义全局变量不好。
【2】寄存器方式 – 简单说就是将数据存放在寄存器中,然后函数要用去寄存器取。
【3】栈方式 – 将数据放在栈中,pop 和 push。
一般的话不用第一种,用第二和第三种。
考试一般用第二种比较简单,实际代码一般使用第三种比较规范。
1. 寄存器传参
案例:插入排序
这里就是采用第二种方式,每次调用函数之前先将参数存入寄存器中,后面函数执行的时候再去寄存器取。比如 ReadInt 等函数就是采用这种方式传参。
C 语言实现:
1 |
|
汇编实现:
1 |
|
注:ret 是函数返回,后面可以接参数。而 exit 是程序退出。
proc 代表函数开始,endp 代表函数结束。
2. 栈传参
案例:判断是否是完美数
注意:栈传参实现函数的时候内心一定要有一个栈,然后还要有 ebp 和 esp 两个指针。
ESP(Extended Stack Pointer),它指向栈顶,也就是当前栈里最上面的位置。当我们需要往栈里放数据时,ESP 会向下移动;当我们需要取出数据时,ESP 会向上移动。
EBP(Extended Base Pointer)则是一个基准指针,它指向当前函数在栈中的起始位置。EBP 的作用就像是给我们一个固定的参照点,方便我们找到函数里的参数和局部变量。在函数调用的过程中,EBP 的值是保持不变的,帮助我们更容易地管理数据。
从 ebp—->esp
地址:低 ——> 高
案例
(1)c 语言算法
1 |
|
(2)汇编实现
1 |
|
栈传参的函数基本格式模板
1 |
|
基础模板
1 |
|
如果有函数中有变量或者返回值的时候在原来的模板上添加相应代码:
1 |
|
补充:栈传递数组参数
将数组首地址存放在栈中,但是有一个问题,前面说过,间接寻址符号 [] 要想通过里面的地址取值,里面必须是寄存器。所以我们每次要想对数组元素操作要先将存入栈中的数组首地址先暂时存放在寄存器中。
案例:获取数组第二个元素的地址
1 |
|
补充: 关于栈传参实现函数里面的局部变量
函数里面的参数可以存放在栈里,也可以存放在寄存器中。
但是如果是作为返回值的变量一般是放在栈中。
十. 写代码技巧
(1)把栈中哪里存什么记在纸上
(2)先写整体,如循环和条件,再写局部
(3)一行代码一行代码翻译