汇编语言

零. 学习介绍和使用工具

【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. 伪指令(由编译器执行)
  3. 其它符号(由编译器识别)

汇编语言的核心是汇编指令,它决定了汇编语言的特性。
具体语法:

1. 注释

汇编语言分号后面是注释。

2. 基本框架

例:

1
2
3
4
5
6
7
8
9
include irvine32.inc    ; 包含 Irvine32 库,提供 32 位输入输出功能
.data ; 定义数据段,这里定义的数据是存储在内存中
... ; 数据定义,这里省略
.code ; 定义代码段
main proc ; 主过程开始
... ; 执行过程,这里省略
exit ; 调用 Irvine32 库中的 exit 函数,程序退出
main endp ; 主过程结束
end main ; 程序结束

四. 数据的定义

在汇编语言中,不同的数据类型用来表示不同长度的数据。常见的数据类型,如:
BYTE:一个字节(8 位)
WORD: 一个字(16 位)
DWORD: 双字(32 位)【也可以写成 dd】
QWORD:四字(64 位)

注:一个字等于两个字节!寄存器是双字。

1
2
.data
a dword 1

如果数值不确定,可以用? 代替值

1
2
.data
a dword ?

五. 通用数据处理指令

1.mov 传送指令

【1】传送指令 mov

传送指令 MOV 把一个字节,字或双字的操作数从源位置传送至目的位置,可以实现常数,通用寄存器,主存(内存)之间的数据的传送。

1
2
mov eax, 10    ; 将立即数10传送到eax寄存器中
mov eax, ebx ; 将立即数从ebx传送到eax中

注意点:(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
LEA reg,mem ; 有效地址送寄存器指令

2. 堆栈操作指令

【1】PUSH 指令

push 指令用于将数据压入栈中。语法如下:

1
2
push operand

其中 operand 可以是寄存器、内存地址或立即数。
(1)如果 operand 是寄存器,则该寄存器中的内容将被压入栈中。
(2)如果 operand 是内存地址,则将该地址处的数据压入栈中。
(3)如果 operand 是立即数,则将该立即数值压入栈中。

1
2
3
push ax ; 将 AX 寄存器中的值压入栈中 
push word [bx] ; 将 BX 寄存器所指向的内存单元中的值压入栈中
push 10 ; 将立即数 10 压入栈中

【2】POP 指令

pop 指令用于从栈中弹出数据。语法如下:

1
2
pop operand

其中 operand 可以是寄存器或内存地址。
(1)如果 operand 是寄存器,则栈顶元素将被弹出并存入该寄存器。
(2)如果 operand 是内存地址,则栈顶元素将被弹出并存入该地址所指向的内存单元。

1
2
pop ax ; 从栈中弹出一个字,并将其存入 AX 寄存器中 
pop word [bx] ; 从栈中弹出一个字,并将其存入 BX 寄存器所指向的内存单元中

注意事项:push 和 pop 操作是成对出现的,每个 push 都应该有一个对应的 pop。

【3】PUSHAD 和 POPAD

1.PUSHAD

在 32 位环境下,PUSHAD 指令将通用寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 的值依次压入栈中。语法如下:

1
2
PUSHAD

执行 PUSHAD 指令后,栈顶元素为 EDI 寄存器的值,依次向下是 ESI、EBP、ESP、EBX、EDX、ECX、EAX。

2.POPAD

在 32 位环境下,POPAD 指令将栈顶的数据弹出并存入通用寄存器 EAX、ECX、EDX、EBX、ESP、EBP、ESI 和 EDI 中。语法如下:

1
2
POPAD

执行 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
2
3
4
5
6
7
8
9
10
11
12
13
14
; 8位无符号整数乘法示例 

MOV AL, 5 ; 将AL寄存器设置为操作数5
MUL BL ; 将BL寄存器的值与AL相乘,结果存储在AX寄存器中

; 16位无符号整数乘法示例

MOV AX, 1000h ; 将AX寄存器设置为操作数1000h
MUL BX ; 将BX寄存器的值与AX相乘,结果存储在DX:AX寄存器对中

; 32位无符号整数乘法示例

MOV EAX, 12345678h ; 将EAX寄存器设置为操作数12345678h
MUL ECX ; 将ECX寄存器的值与EAX相乘,结果存储在EDX:EAX寄存器对中

(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
2
3
4
5
6
7
8
9
mov eax, 17
mov edx, 0
;注:div后面也要是寄存器
mov ecx, 4
div ecx

call writeInt ;输出商
mov eax, edx
call writeInt ;输出余数

注意一定要将后面存余数的 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
2
3
4
5
6
7
8
9
10
11
12
13
14
include irvine32.inc  ; 包含 Irvine32 库

.data
hello_msg byte "Hello World!", 0 ; 存储要显示的字符串,字符串要定义成byte字节类型
.code
main proc
mov edx,offset hello_msg ; 将要显示的字符串地址存储在 edx
; 上面这句也可以使用地址传送指令,实现同样效果--将hello_msg地址传入edx
; lea edx, hello_msg
call WriteString ; 调用 Irvine32 函数 WriteString 输出字符串
exit ; 退出程序
main endp

end main

注:.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
2
3
;输出空格
mov al, ' '
call WriteChar

(4)输出换行

call crlf       ; 输出换行

七. 定义数组

1. 定义数组

1
2
.data
a dd 1,2,3,4,5,6,7

如果要数组一次性赋值:

1
2
.data
a dd 10 dup(0)

2. 访问数组

访问第一个元素

1
2
3
4
5
6
7
8
9
include irvine32.inc
.data
a dword 1, 2, 3, 4, 5, 6, 7
.code
start:
mov eax, a
call writeint
exit
end start

注:writeint 的本质是读取 eax 的四个字节,然后将这四个字节的数据转化为 int 值。
mov eax, a 是将第一个元素的值放入 eax 中。

数组第 i 个元素访问:

1
2
3
4
5
6
7
8
9
10
include irvine32.inc
.data
a dword 1, 2, 3, 4, 5, 6, 7
.code
start:
;访问第三个元素
mov eax, a+2*4
call writeint
exit
end start

还有一种 a+i4 更加好看更接近 c++ 的写法就是 a[i4],一般我们都这样写。
注:[]这个加不加知识规范的问题,a+i4 和 [a+i4] 实际上和 a[i*4]是一致的。
这里加减的单位是字节。

1
2
3
4
5
6
7
8
9
include irvine32.inc
.data
a dword 1, 2, 3, 4, 5, 6, 7
.code
start:
mov eax, a[i*4]
call writeint
exit
end start

3.[]– 间接寻址

【1】如果 [] 里面是寄存器,则会根据寄存器中存储的内存地址取得对应的值。

1
2
lea ebx, a        ; 将a的内存地址移入ebx中
mov eax, [ebx] ; 通过ebx中的地址指向的变量的值移入eax中

【2】如果 [] 里面是内存变量(不管是全局还是栈中)或立即数,则和没加一样。

比如下面这两个指令的地址是一样的。

1
2
3
;下面这两个语句作用一样,都是取出a的值或者取出a数组首地址的值
mov eax, a
mov eax, [a]

八. 分支和循环结构的汇编实现

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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
c++中的写法
if(a<b){
Block1
}else{
Block2
}

转化为比较和跳转指令实现

if a<b goto L1
Block2
goto final
L1:
Block1
final:

但是上面并没有Block1和Block2的顺序并没有和我们c++的正常逻辑一一对应
当我们真正去写的时候会发现有点麻烦,一般写的时候会将比较条件反转,也就是像下面这样

if a>=b goto ifElse
Block1
goto ifEnd
ifElse:
Block2
ifEnd:

最终模板:

(1)if-else

1
2
3
4
5
6
7
8
9
10
mov eax, a
mov ebx, b
cmp eax, ebx
jge ifElse
...
jmp ifEnd
ifElse:
...
ifEnd:
...

(2)if

1
2
3
4
5
6
7
mov eax, a
mov ebx, b
cmp eax, ebx
jge ifEnd
...
ifEnd:
...

分支结构案例:

求整数 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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
c语言中:
for(i=0;i<n;i++){
Block;
}

循环的本质是if判断:
if(i<n){
Block;
i++;
}

i=0
L0: if(i<n) goto L1
goto final
L1: Block
i++
jmp L0
final:

将比较条件反转,更简单书写,简化代码
i=0;
startFor: if(i>=n)
goto endFor
Block
i++
jmp startFor
endFor:

循环模板:

1
2
3
4
5
6
7
8
9
10
11
.data
n dd 10
.code
mov esi, 0
startFor:
cmp esi, n
jge endFor
...
inc esi
jmp startFor
endFor:

循环结构案例

在内存中存有 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. 用于模块化、是重要的封装机制
  2. 函数定义方式与执行逻辑
  3. 参数传递方法:
    【1】内存变量 (数据段) 方式 – 简单说就是在. data 下定义全局变量,所有函数都可以用。一般定义全局变量不好。
    【2】寄存器方式 – 简单说就是将数据存放在寄存器中,然后函数要用去寄存器取。
    【3】栈方式 – 将数据放在栈中,pop 和 push。

一般的话不用第一种,用第二和第三种。
考试一般用第二种比较简单,实际代码一般使用第三种比较规范。

1. 寄存器传参

案例:插入排序

这里就是采用第二种方式,每次调用函数之前先将参数存入寄存器中,后面函数执行的时候再去寄存器取。比如 ReadInt 等函数就是采用这种方式传参。

C 语言实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
#include <stdio.h>
#include <stdlib.h>

#define SIZE 20

void insertSort(int arr[], int size);
void outputArray(int arr[], int size);
void buildArray(int arr[], int size);

int main() {
int arr[SIZE];

buildArray(arr, SIZE);
outputArray(arr, SIZE);
printf("\n");

insertSort(arr, SIZE);
outputArray(arr, SIZE);

return 0;
}

void insertSort(int arr[], int size) {
for (int i = 1; i < size; i++) {
int key = arr[i];
int j = i - 1;
while (j >= 0 && arr[j] > key) {
arr[j + 1] = arr[j];
j--;
}
arr[j + 1] = key;
}
}

void outputArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
printf("%d ", arr[i]);
}
}

void buildArray(int arr[], int size) {
for (int i = 0; i < size; i++) {
arr[i] = rand() % 1000;
}
}

汇编实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
include irvine32.inc

.data
arr1 dword 10 dup(?) ; 第一个数组大小为10
arr2 dword 20 dup(?) ; 第二个数组大小为20

.code
main proc
; 调用buildArray填充数组
mov edx, offset arr2 ; 将arr2的地址存入edx
mov ecx, 20 ; 设置数组大小为20
call buildArray ; 调用buildArray填充数组

; 输出填充后的数组
mov edx, offset arr2 ; 将arr2的地址存入edx
mov ecx, 20 ; 设置数组大小为20
call outputArray ; 调用outputArray输出数组
call crlf ; 输出一个换行符

; 调用insertSort对数组进行排序
mov edx, offset arr2 ; 将arr2的地址存入edx
mov edi, 20 ; 设置数组大小为20
call insertSort ; 调用insertSort排序数组

; 输出排序后的数组
mov edx, offset arr2 ; 将arr2的地址存入edx
mov ecx, 20 ; 设置数组大小为20
call outputArray ; 调用outputArray输出数组

exit
main endp

; 插入排序过程
insertSort proc
mov ebx, 1 ; 设置循环计数器ebx为1

startFor:
cmp ebx, edi ; 比较ebx与数组大小
jge endFor ; 如果ebx >= 数组大小,跳出循环

; 将当前元素保存到ecx中
mov ecx, [edx + 4 * ebx]
mov esi, ebx ; 将当前位置保存到esi中
sub esi, 1 ; esi减1,指向前一个元素

startWhile:
cmp esi, 0 ; 比较esi与0
jl endWhile ; 如果esi < 0,跳出循环

; 比较当前元素与前一个元素
cmp [edx + 4 * esi], ecx
jle endWhile ; 如果前一个元素小于等于当前元素,跳出循环

; 将前一个元素后移一位
mov eax, [edx + 4 * esi]
mov [edx + 4 * esi + 4], eax
sub esi, 1 ; 指向前一个位置
jmp startWhile ; 继续循环

endWhile:
; 插入当前元素到正确的位置
mov [edx + 4 * esi + 4], ecx
add ebx, 1 ; ebx加1,指向下一个元素
jmp startFor ; 继续循环

endFor:
ret
insertSort endp

; 输出数组过程
outputArray proc
mov esi, 0 ; 设置循环计数器esi为0

startFor:
cmp esi, ecx ; 比较esi与数组大小
jge endFor ; 如果esi >= 数组大小,跳出循环

; 输出当前元素并加上空格
mov eax, [edx + 4 * esi]
call writedec
mov al, ' '
call writechar
add esi, 1 ; esi加1,指向下一个元素
jmp startFor ; 继续循环

endFor:
ret
outputArray endp

; 填充数组过程
buildArray proc
mov esi, 0 ; 设置循环计数器esi为0

startFor:
cmp esi, ecx ; 比较esi与数组大小
jge endFor ; 如果esi >= 数组大小,跳出循环

; 生成随机数并存入数组
mov eax, 1000
call randomRange
mov [edx + 4 * esi], eax
add esi, 1 ; esi加1,指向下一个位置
jmp startFor ; 继续循环

endFor:
ret
buildArray endp

end main

注: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
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>
int isPerfNumber(int n) {
int sum=1;
int factor=2;
while (factor < = n/2) {
if ( n% factor ==0 ) {
sum = sum + factor;
}
factor=factor+1;
}
if (sum==n) return 1;
return 0;
}
int main() {
int n;
scanf("%d", &n);
if (isPerfNumber(n)) {
printf("+1", n);
} else {
printf("+0", n);
}
return 0;
}

(2)汇编实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
include irvine32.inc
.data
.code
main proc
;判断是否是完美数并输出
;输入n,此时ebx为n
call readInt
mov ebx, eax

;将ebx存入栈中完成传参
push ebx
call isPerfNumber
pop ebx

;输出结果,函数执行的最后将结果存储在eax中
call writeInt
exit
main endp



;判断是否是完美数
isPerfNumber proc

; 保存初始ebp值
push ebp

; 保存初始esp值,此时n为[ebp+8]
mov ebp,esp

; 开辟12个字节的空间,esp指针向下移动
sub esp, 12
; 存储sum和factor两个变量
mov dword ptr [ebp-4], 1 ; sum = 1 = [ebp-4]
mov dword ptr [ebp-8], 2 ; factor = 2 = [ebp-8]

; 将所有寄存器进栈,为什么要有这一步呢?这是一种规范,为的是后面出栈的话可以将寄存器
; 函数结束的时候可以恢复到调用函数之前的状态,使得调用函数前后寄存器的值不变
pushad



;循环计算真因子之和,中间的过程可以省略不看,直接跳到最后
startWhile:
mov eax, [ebp+8] ; eax=n
mov edx, 0
mov ecx, 2 ; ecx=2
div ecx ; n/2
cmp [ebp-8], eax ; eax存放n/2
jg endWhile
mov eax, [ebp+8] ;eax存放n

mov ecx, [ebp-8]
div ecx
cmp edx, 0 ;余数是0
je sumplus
jmp whileFinal
sumplus:
mov ecx, [ebp-8]
add [ebp-4], ecx
jmp whileFinal
whileFinal:
mov ecx, [ebp-8]
inc ecx
mov [ebp-8], ecx
jmp startWhile
endWhile:
;判断sum和n是否相等从而决定是返回1还是返回0
mov ecx, [ebp+8]
cmp [ebp-4], ecx ;sum==n
je return1
jmp return0


;清理内存并退出函数,将值存在eax

return1:
; 注意:出栈顺序要与入栈顺序相同!!!!因为pushad会将ebp和esp入栈,
; 如果出栈popad顺序不对会导致这两个指针值出错,造成程序崩溃
popad
;释放开辟变量的空间
add esp, 12
;还原esp
mov esp, ebp
;还原ebp
pop ebp

mov eax, 1
ret

return0:
popad
add esp, 12
mov esp, ebp
pop ebp

mov eax, 0
ret
isPerfNumber endp

end main

栈传参的函数基本格式模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
.code
main proc
push n
call func
pop eax
exit
main endp

func proc

; 保存初始ebp值
push ebp
; 保存初始esp值,此时n为[ebp+8] ;这里加减的单位是字节
mov ebp,esp
; 将所有寄存器进栈,为什么要有这一步呢?这是一种规范,为的是后面出栈的话可以将寄存器
; 函数结束的时候可以恢复到调用函数之前的状态,使得调用函数前后寄存器的值不变
pushad

...

; 还原寄存器的值
popad
;还原esp
mov esp, ebp
;还原ebp
pop ebp

ret
func endp
end main

基础模板

1
2
3
4
5
6
7
8
9
10
11
12
13
14
func proc
push ebp
mov ebp,esp
; 保存初始esp值,此时n为[ebp+8] ;这里加减的单位是字节
pushad

...

popad
mov esp, ebp
pop ebp
ret
func endp
end main

如果有函数中有变量或者返回值的时候在原来的模板上添加相应代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
func proc
push ebp
mov ebp,esp

sub esp, 12
; 例:存储sum和factor两个变量
mov dword ptr [ebp-4], 1 ; sum = 1 = [ebp-4]
mov dword ptr [ebp-8], 2 ; factor = 2 = [ebp-8]


pushad

...

popad

add esp, 12

mov esp, ebp
pop ebp

;如果有返回值可以最后放在寄存器中
mov eax, 0

ret
func endp

补充:栈传递数组参数

将数组首地址存放在栈中,但是有一个问题,前面说过,间接寻址符号 [] 要想通过里面的地址取值,里面必须是寄存器。所以我们每次要想对数组元素操作要先将存入栈中的数组首地址先暂时存放在寄存器中。

案例:获取数组第二个元素的地址

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
include irvine32.inc  
.data
data dd 300,6789,-1,100,-255,99,-300,-6789
.code
main proc
push offset data
call getSecondElement
pop eax
exit
main endp
getSecondElement proc
; 保存初始ebp值
push ebp
; 保存初始esp值,此时data为[ebp+8]
mov ebp,esp
; 将所有寄存器进栈
pushad

mov ebx, [ebp+8]
mov eax, [ebx+4*1]
call writeInt

; 还原寄存器的值
popad
;还原esp
mov esp, ebp
;还原ebp
pop ebp
ret
getSecondElement endp
end main

补充: 关于栈传参实现函数里面的局部变量

函数里面的参数可以存放在栈里,也可以存放在寄存器中。
但是如果是作为返回值的变量一般是放在栈中。

十. 写代码技巧

(1)把栈中哪里存什么记在纸上
(2)先写整体,如循环和条件,再写局部
(3)一行代码一行代码翻译


汇编语言
https://flyingpig.fun/2023/10/13/汇编语言 (Assemble Language)学习笔记/
作者
flyingpig
发布于
2023年10月13日
许可协议