微机原理学习笔记

开始之前

好吧,不太清楚究竟是怎么回事,但是貌似我们学校的《微机原理》改名成了《汇编与接口》。汇编这玩意儿还是有点意思的,就是写起来对人来说太不友好了( 不过除了非常底层的优化外,貌似手撸汇编在现在的意义没那么大(?

实验环境搭建

环境搭建也算是老生常谈的话题了。。基本上每学一门新语言都要经历这个步骤。

注意这次我们所需的是 32 位的 masm 环境,所以我们需要 visual studio 来提供 msvc 工具链。

由于部分用于代码高亮和自动补全的的插件仍然没有适配 vs2022,所以我们需要安装 vs2019

这是下载链接,注意,你得注册一个微软账户才能在上面下载,简单来说就是去注册一个 outlook 邮箱。

如图所示,在安装时勾选"使用c++的桌面开发"即可。

在安装后打开 vs,进入项目后点击顶栏的拓展-管理拓展,然后搜索 asmdude 并点击安装,然后关闭并重新进入 vs 即可启用拓展 。

接下来新建一个空项目然后按照下图操作。

...并点击确定。 右键右侧的源文件并新建一个 main.asm,当然命名是无所谓的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.386    
.model flat, c
option casemap :none

includelib libcmt.lib
includelib legacy_stdio_definitions.lib

EXTERN printf :PROC ; declare printf

.data
HelloWorld db "Hello World!:-)", 0

.code
main PROC
push offset HelloWorld
call printf
add esp, 4
ret
main ENDP
END

注意我们现在是在编写32位的汇编,所以请确保在 x86Debug 模式下编译和调试,然后点击顶栏的绿色三角。

到此就完成了环境的搭建。

零碎小玩意儿

基本上这节就是些小知识点,因为编排问题没有加进正文里,所以统一放这儿了。

IA-32中的数据单位

大小(Bits) 单位
1 比特 (Bit)
4 半字节 (Nibble)
8 字节 (Byte)
16 字 (Word)
32 双字 (Double Word)
64 四字 (Quad Word)

stdcall 调用约定

stdcall 是一种常见的调用约定,其中函数的参数通过栈传递,函数的参数是从右到左依次推送到栈上,而栈上的每个参数占据4字节的空间。

琐碎的指令们

跳转指令

跳转指令 功能
jmp unconditional jump
je jump if equal
jne jump if not equal
jl jump if less
jg jump if greater
jle jump if less or equal
jge jump if greater or equal
jo jump if overflow
jno jump if not overflow
js jump if sign (比较结果为负数)
jns jump if not sign
jc jump if carry (产生进位)
jnc jump if not carry

值得注意的是,有符号数使用 jgjl,而无符号数使用 jajb

除法指令 div

这一节为语言模型生成内容,参考 promt 如下

1
请介绍 IA-32 masm 中的 div 指令并举一个用其判断奇偶数的例子

div指令是IA-32汇编语言中的除法指令,用于将一个32位的无符号整数除以一个操作数,并将商存储在EDX:EAX寄存器对中的EAX寄存器中,余数存储在EDX寄存器中。

div指令的基本语法如下:

1
div operand

其中,operand表示除数。

为了判断一个数是奇数还是偶数,可以使用div指令来计算除以2的余数。如果余数为0,则说明被除数是偶数;如果余数为1,则说明被除数是奇数。

下面是一个使用div指令判断奇偶数的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
mov eax, 13 ; 被除数,例如,我们判断13是奇数还是偶数
mov edx, 0 ; 清零EDX寄存器,用于存储余数

mov ecx, 2 ; 除数,即2
div ecx ; 将eax中的值除以ecx,商存储在eax,余数存储在edx

cmp edx, 0 ; 比较余数和0
jne odd ; 如果余数不等于0,跳转到odd标签
; 偶数的处理
; 偶数的代码逻辑
jmp done ; 跳转到done标签结束

odd: ; odd标签
; 奇数的处理
; 奇数的代码逻辑

done: ; done标签

在上述示例中,我们将13存储在eax寄存器中作为被除数,将2存储在ecx寄存器中作为除数。然后使用div ecx指令进行除法运算,商存储在eax寄存器中,余数存储在edx寄存器中。接下来,我们通过比较余数和0的值来判断奇偶性,如果余数不等于0,则跳转到odd标签,执行奇数的处理逻辑;如果余数等于0,则跳转到done标签,执行偶数的处理逻辑。

这是一个简单的例子,演示了如何使用div指令来判断一个数的奇偶性。根据实际需求,您可以根据eax寄存器的值进行不同的处理。

交换指令 xchg

此节为语言模型生成内容,参考 prompt 如下

1
介绍 IA-32 masm 中的 xchg 指令并举一个用其交换数据的例子

xchg指令是IA-32汇编语言中的交换指令,用于交换两个操作数的值。它可以用于交换寄存器、内存和寄存器之间的数据。

xchg指令的基本语法如下:

1
xchg destination, source

其中,destinationsource表示要交换值的操作数。

下面是一个使用xchg指令交换两个变量值的示例:

1
2
3
4
5
6
mov eax, 10  ; 变量A的初始值为10
mov ebx, 20 ; 变量B的初始值为20

xchg eax, ebx ; 交换变量A和变量B的值

; 现在eax中存储的是交换后的变量B的值,ebx中存储的是交换后的变量A的值

在上述示例中,我们使用mov指令将变量A的初始值10存储在eax寄存器中,将变量B的初始值20存储在ebx寄存器中。然后使用xchg指令交换eaxebx的值。交换后,eax寄存器中存储的是变量B的值20,ebx寄存器中存储的是变量A的值10。

通过xchg指令,我们可以方便地交换两个变量的值,而不需要借助额外的寄存器或内存位置。这在某些算法或数据处理任务中可能会有用。

逻辑与指令 test

此节使用语言模型生成,prompt 格式同前节

test指令是IA-32汇编语言中的逻辑与(AND)操作指令。它执行两个操作数之间的位逻辑与运算,并根据结果设置标志寄存器的状态。

test指令的基本语法如下:

1
test destination, source

其中,destinationsource表示要进行逻辑与运算的操作数。

test指令执行以下操作:

  1. destinationsource进行逻辑与运算,将结果存储在临时位置中。
  2. 根据结果设置标志寄存器的状态,特别是设置零标志位(ZF)和进位标志位(CF)。

test指令常用于条件判断、位掩码和清零操作等。

下面是一些test指令的使用示例:

1
2
3
4
5
6
7
8
9
10
11
mov eax, 10
test eax, eax ; 检查eax寄存器是否为零

mov ebx, 0xFF
test ebx, 0x0F ; 检查ebx寄存器的低4位是否都为1

test edx, edx ; 检查edx寄存器是否为零
jz zero ; 如果为零,跳转到zero标签处

zero:
; 执行一些操作

在第一个示例中,将常数10存储在eax寄存器中,然后对eax寄存器和自身执行逻辑与运算。这将检查eax寄存器的值是否为零,并根据结果设置标志寄存器的状态。

在第二个示例中,将常数0xFF存储在ebx寄存器中,然后对ebx寄存器和常数0x0F执行逻辑与运算。这将检查ebx寄存器的低4位是否都为1,并根据结果设置标志寄存器的状态。

在第三个示例中,将edx寄存器与自身执行逻辑与运算。这将检查edx寄存器的值是否为零,并根据结果设置标志寄存器的状态。如果edx寄存器为零,将会发生跳转到zero标签处执行相应的操作。

通过test指令,我们可以执行逻辑与运算并根据结果设置标志寄存器的状态,以便进行条件判断和控制程序的流程。

处理器结构

基本结构

  1. 算术逻辑单元 (ALU)
  2. 寄存器
  3. 指令处理单元

寄存器

寄存器就是 CPU 暂时存取数据的地方,在 IA-32 基本执行环境中包括8个32位通用寄存器,6个12位段寄存器,以及32位标志寄存器和指令指针。

常用寄存器

通用寄存器

通用寄存器一般是指处理器中最长使用的整数存取寄存器,可以用于保存整型数据和地址等。

在上图的寄存器展示中,这些32位寄存器也可以当作16位寄存器使用,只需要去除寄存器名前的 "E" 即可,例如 AX, BX, SI, DI 等等。

甚至可以将寄存器的低16位拆成2个8位寄存器使用,例如图中 EAXD15-D8 就对应着 AH ,而低八位的 D7-D0 则对应着 AL 寄存器。

通用寄存器的常用用途

  1. EAX: 最常用的寄存器,函数调用传参,作为累加器,算数运算和外设传送信息等
  2. EBX: 基址寄存器,长用来存放存储器地址
  3. ECX: 计数器
  4. EDX: 数据寄存器,可用来存放数据
  5. ESI: 源变址寄存器,用于指向字符串或数组的源操作数。源操作数是指保存传送结果或运算结果的寄存器。
  6. EDI: 目的变址寄存器,用于指向字符串或数组的目的操作数,目的操作数是指保存传送结果或者运算结果的操作数。
  7. EBP: 基址指针寄存器,默认指向程序堆栈区域的数据,主要用于在子程序中访问通过堆栈传递的参数和局部变量。
  8. ESP: 堆栈指令寄存器,专门用于指向程序堆栈区域顶部的数据,在涉及堆栈操作时会自动变化。

标志寄存器

标志寄存器

OF 溢出标志、SF 符号标志、ZF 零标志、PF 奇偶标志、CF 进位标志、AF调整标志

专用寄存器

  1. 指令寄存器 EIP: 保存将要执行指令的地址
  2. 段寄存器: CS DS SS ES FS GS
  3. 浮点寄存器、向量寄存器、媒体寄存器、系统寄存器等。

存储器组织

存储器模型

  1. 平展存储模型:存储器是连续的地址空间,每个存储单元存储一个字节并拥有一个线性地址,IA-32 处理器最大支持 \(2^{32} - 1\) (4GB) 的地址空间。
  2. 段式存储模型: 存储器由一组独立的地址空间(段)组成,代码、数据、堆栈位于分开的段中,程序利用逻辑地址寻址段中的字节,IA-32 支持 \(2^{14}\) 个大小类型各异的段,段的最大容量与平展存储相同,均为 4GB 。
  3. 实地址存储模型: 8086 的存储模型,是段式存储的特例,线性地址空间最大位 1MB ,由最大为 64KB 的多个段组成。

工作方式

保护方式(Protected Mode)

  1. IA-32处理器固有的工作状态
  2. 具有强大的段页式存储管理和特权与保护能力
  3. 使用全部32条地址总线,可寻址4GB物理存储器
  4. 使用平展或段式存储模型
  5. 利用虚拟8086方式支持实地址8086软件

实地址方式(Real Address Mode)

  1. 可以进行32位处理的快速8086
  2. 只能寻址1MB物理存储器空间,每个段不超过64KB
  3. 可以使用32位寄存器、32位操作数和32位寻址方式
  4. 只能支持实地址存储模型

系统管理方式(System Management Mode, SMM)

  1. 用于实现节能和系统安全管理
  2. 保存当前任务后切换到分开的地址空间

逻辑地址

逻辑地址是程序设计和处理器中使用的相对地址,逻辑地址 = 段基地址:偏移地址,编程使用的逻辑地址会被处理器映射为线性地址,在输出前转换为物理地址。

基本段

  1. 代码段: 存放指令代码
  2. 数据段: 存放数据
  3. 堆栈段: 堆栈数据
基本段的逻辑地址
  1. 代码段: CS:EIP
  2. 数据段: DS/FS/GS:取决于寻址方式,由存储器计算得出
  3. 堆栈段: SS:ESP

段选择器

逻辑地址的段基地址部分由16位的段寄存器确定。段寄存器保存16位的段选择器,段选择器是一种特殊的指针,指向段描述符,段描述符包括段基地址,由段基地址就可以指明存储器中的一个段。

根据存储模型不同,段寄存器的具体内容也有所不同。

平展存储模型
  1. 6个段寄存器均指向线性地址空间的0地址
  2. 程序设置2个重叠的段,一个用于代码,一个用于数据和堆栈,CS段寄存器指向代码,其他段寄存器指向数据和堆栈段。
段式存储模型

段寄存器保存不同的段选择器,指向线性地址空间不同的段,例如下图所示,程序最多可以访问6个段,CS指向代码段,SS指向堆栈段,DS和其余4个段寄存器指向的数据段。

image.png
实地址存储模型

实地址存储的主存只有 1MB(\(2^{20}\)字节),仅使用地址总线的低20位,物理地址范围为 00000H-FFFFFH。实地址存储模型也进行分段处理,但是存在2个限制:

  1. 每个段最大64KB
  2. 段只能开始于低4位地址全为0的物理地址处,因此实地址的段寄存器可以保存段基地址的高16位

地址转换

保护方式的地址转换
存储模型 转换方式
平整存储模型 段基地址为0,偏移地址等于线性地址
段式存储模型 段基地址和偏移地址均为32位
端基加上线性偏移等于线性地址

此外,在不使用分页机制时,线性地址与物理地址一一对应,使用分页时,线性地址空间被分成大小一致的块(页),构成虚拟存储器并转换到物理地址空间。

实地址方式的地址转换

汇编语言/实验混合

这块照搬书或者 PPT 就有点太无聊了,所以决定混合着上机实验一块复习了。

masm 模板

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
.586 ;该行指令设置使用 586 或更高版本的指令集
.MODEL flat,stdcall ;该行指令设置使用 flat 模型和 stdcall 调用约定

;这些行指令将相关的库文件包含到程序中,以便使用库中定义的函数和符号。
includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

;下面5行指令声明了一些常用的函数原型
ExitProcess PROTO, dwExitCode:DWORD
printf proto C: vararg
scanf PROTO C : vararg

_getch PROTO C : vararg
_kbhit PROTO C : vararg

;include temp.inc

.data
;在此处可以声明一些变量,类似于高级语言中的 static

.code
main PROC
;main:
;此处编写主程序

push 0;将0压栈
call ExitProcess;对应return 0
main ENDP
end main

实验二:简单分支程序设计

实验目的:

  1. 掌握算术运算指令
  2. 掌握 cmp, test 以及各类转移指令
  3. 掌握 c 语言关系表达式和逻辑表达式对应的汇编实现
  4. 掌握分支程序设计

1. 判断输入

将下面C语言程序的代码片段转换为功能等价的汇编语言代码片段;编写一完整的汇编语言程序验证转换的正确性,其中sinteger(输入的整数) 与sign(输出的信息)均为双字变量。

1
2
3
4
5
6
if  (sinteger == 0)
sign = 0;
else if ( siteger > 0)
sign = 1;
else
sign = -1;

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg

_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
sinteger dd 0;
sign dd 0;
string db '%d', 0;

.code
main PROC
;main:
push offset sinteger
push offset string
call scanf
add esp, 8

cmp sinteger, 0
je zero
jl negtive
;postive:
mov sign, 1
jmp print
zero:
mov sign, 0
jmp print
negtive:
mov sign, -1
print:
push sign
push offset string
call printf
add esp, 8

push 0
call ExitProcess
main ENDP
end main

感觉这个语法和 SHENZHENG/IO 里面限制20行的汇编也差不多(

不过里面用来判断的关键词是 teq/tgt/tlt/tcp

游戏截图

稍微解释一下在这个实验中用到的 IA-32 masm 指令的用法

.data 数据段我们声明了两个初始化0的用于存储输入数字的双字变量,以及一个存入了格式化字符串的byte数组。

在代码段,使用 offset/addr 关键字获取偏移并压栈,并使用 call 关键字调用 scanf 函数读入,由于使用 stdcall 调用约定,所以栈指针 esp 上移 \(2 \times 4 = 8\) 个字节。

此处一种更简单的写法是使用 invoke 宏来处理,这个宏的用法会在之后的实验中介绍。

完成读入后使用 cmp 指令判断其与0的大小关系,并使用条件跳转指令 je(jump if equal), jl(jump if less) 与标签跳转至对应的分支。

并处理完对应分支的操作后重新跳转回主控制流,对应 jmp print

由于读入和输出的格式化字符串一致,所以此处就复用了,接下来的操作流程和开始读入时一致,故此处就不详细介绍了。

2.转换ASCII大小写

将下面C语言程序的代码片段转换为功能等价的汇编语言代码片段,其中 ch1 (输入的字符)与 caps (输出的信息)均为字节变量。

1
2
3
4
if (ch1> =’a’ && ch1< =’z’)
caps=0; ch1=upper(ch1);
if (ch1> =’A’ && ch1< =’Z’)
caps=1;

提示:小写字母与大写字母的 ASCII 码的差值为 20H,也可以用 ch1 = ch1&0x5f

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg

_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
ch1 byte 'a'
cha byte 95
caps byte '0'
infstring db "%c", 0
outfstring db "cha='%c' cap='%c'", 0
errfstring db "error", 0

.code
main PROC
;main:
invoke scanf, addr infstring, addr ch1
mov al, ch1

cmp al, 'a'
jb captial
cmp al, 'z'
ja captial
;small:

and al, cha
mov ch1, al
jmp print

captial:
cmp al, 'A'
jb error
cmp al, 'Z'
ja error

mov caps, '1'
print:
invoke printf,addr outfstring , ch1, caps
ret

error:
invoke printf,addr errfstring
ret
main ENDP
end main

这一段代码中使用 invoke 宏来简化手动管理堆栈平衡的操作,并且使用了 ret 来返回控制权,同时为了处理输入字符可能存在的非法情况,加入了错误处理部分。

3. 100以内的偶数求和

将下面C语言程序的代码片段转换为功能等价的汇编语言代码片段;编写一完整的汇编语言程序验证转换的正确性,其中 sumi 均为双字变量。

1
2
3
4
sum=0; 
for (i=1; i<=100; i++)
if(i%2==0)
sum=sum+i;

提示:可采用 div 指令求余数

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg
_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
fstr db 'sum = %d',0

.code
main PROC
;main:
mov eax, 1;i
mov ebx, 0;sum
mov ecx, 0;temp

_loop:
mov ecx, eax
cmp ecx, 100
ja _end

and ecx, 1
cmp ecx, 0
jnz _no_add
add ebx, eax
_no_add:

add eax, 1
jmp _loop
_end:

invoke printf, addr fstr, ebx
ret
main ENDP
end main

此处采用了一个 trick 来判断奇偶性,对于一个二进制原码整数来说,其奇偶性取决于其 0 位是否为 0,即结尾是 0 的必然是偶数,否则为奇数。

所以 and ecx, 1 如果此时 ecx 中结果为 1,说明其必然为奇数,不需要相加。

4. 100以内的正数求和

采用无条件 jmp 和条件转移 jcc 指令构造 whiledo while 循环结构(不得使用 loop 指令),完成下面的求和任务并输出 sumn (变量为双字) (实际上是条件控制的循环) \[ sum=\sum_{i=1}^{100}i \] 思考题:假设 sum 为双字无符号整数,在 sum 不溢出的情况下求出 n 的最大值;并输出此时的 sumn 的值。

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg
_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
fstr db 'sum = %d', 0

.code
main PROC
;main:
mov eax, 1 ; i
mov ebx, 0 ; sum

_loop:
cmp eax, 100
ja _end
add ebx, eax
add eax, 1
jmp _loop
_end:

invoke printf, addr fstr, ebx
ret
main ENDP
end main

思考题回答

1
2
3
4
5
6
7
8
9
10
const max = 2**32 -1
const f = (n) => (n * (n+1)) /2.0
let n = 1;
while(n++){
if(f(n) > max) {
break;
}
}
console.log("n = ", n)
console.log("sum = ", f(n))

使用 node 或者浏览器控制台运行即可,答案是 n = 92682, sum = 4295022903

5. 三数排序

从键盘上输入3个有符号的双字整数,编写一完整的程序按照又大到小的顺序输出这3个数。

提示:采用 xchg 指令交换两个变量的值。

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg
_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
format db '%d %d %d', 0
_a dd 0
_b dd 0
_c dd 0

.code
main PROC
;main:
invoke scanf, offset format, addr _a, addr _b, addr _c
mov eax, _a
mov ebx, _b
mov ecx, _c

cmp eax, ebx
jnl l1
xchg eax, ebx
l1:

cmp eax, ecx
jnl l2
xchg eax, ecx
l2:

cmp ebx, ecx
jnl l3
xchg ebx, ecx
l3:

invoke printf, offset format, eax, ebx, ecx
invoke ExitProcess,0
main ENDP
end main

6. 判断闰年

输入年份并判断其是否是闰年,闰年需要满足以下条件中的一个:

  1. 该年份能被 4 整除同时不能被 100 整除;
  2. 该年份能被400整除。

提示:可采用 div 指令求余数

参考代码

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
.586
.MODEL flat,stdcall

includelib kernel32.lib
includelib ucrt.lib
includelib legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf PROTO C : vararg
_getch PROTO C : vararg
_kbhit PROTO C : vararg
;include temp.inc

.data
fistr db '%d', 0
fostr1 db 'is leep year',0
fostr2 db 'is not leep year', 0

.code
main PROC
;main:
push ebp
mov ebp, esp

sub esp, 8
mov dword ptr [ebp], 0;栈上分配int, 初始化为0
lea eax, [ebp-8]

invoke scanf, addr fistr, eax
mov ecx, dword ptr [ebp-8]

mov eax, ecx
mov ebx, 400
cdq
idiv ebx
cmp edx, 0
jz is_leap;year mod 400 == 0

mov eax, ecx
mov ebx, 4
cdq
idiv ebx
cmp edx, 0
jne not_leap;year mod 4 == 0

mov eax, ecx
mov ebx, 100
cdq
idiv ebx
cmp edx, 0
jne is_leap;year mod 100 != 0

not_leap:
invoke printf,addr fostr2
jmp _end

is_leap:
invoke printf,addr fostr1

_end:
add esp, 8
mov esp, ebp
pop ebp

mov eax, 0
ret
main ENDP
end main

此处代码的分析使用语言模型生成,参考 prompt 如下

1
2
逐行分析下列 IA-32 masm 代码中 .code 段的意图
// 代码省略

分析

该汇编代码段中的.code段是用于定义程序的主要代码逻辑和流程。下面逐行分析该代码段的意图:

1
2
.code
main PROC

开始定义一个名为main的过程(Procedure)。

1
2
push ebp
mov ebp, esp

保存旧的基址寄存器值并将当前栈指针赋值给基址寄存器,创建一个新的堆栈帧。

1
2
3
sub esp, 8
mov dword ptr [ebp], 0
lea eax, [ebp-8]

在堆栈上分配8字节的空间,并将该空间的第一个双字(32位整数)初始化为0。然后,将eax寄存器设置为指向这个双字的地址。

1
2
invoke scanf, addr fistr, eax
mov ecx, dword ptr [ebp-8]

调用scanf函数,从用户输入中读取一个整数,并将其存储到之前分配的空间中。然后,将ecx寄存器设置为该整数的值。

1
2
3
4
5
6
mov eax, ecx
mov ebx, 400
cdq
idiv ebx
cmp edx, 0
jz is_leap

eax寄存器设置为ecx的值,然后将ebx寄存器设置为400,执行带符号除法指令idiv,将eax寄存器中的值除以ebx寄存器中的值。将得到的余数存储在edx寄存器中,然后将edx与0进行比较。如果相等(余数为0),则跳转到is_leap标签处。

1
2
3
4
5
6
mov eax, ecx
mov ebx, 4
cdq
idiv ebx
cmp edx, 0
jne not_leap

eax寄存器设置为ecx的值,然后将ebx寄存器设置为4,执行带符号除法指令idiv,将eax寄存器中的值除以ebx寄存器中的值。将得到的余数存储在edx寄存器中,然后将edx与0进行比较。如果不相等(余数不为0),则跳转到not_leap标签处。

1
2
3
4
5
6
mov eax, ecx
mov ebx, 100
cdq
idiv ebx
cmp edx, 0
jne is_leap

eax寄存器设置为ecx的值,然后将ebx寄存器设置为100,执行带符号除法指令idiv,将eax寄存器中的值除以ebx寄存器中的值。将得到的余数存储在edx寄存器中,然后将edx与0进行比较。如果不相等(余数不为0),则跳转到is_leap标签处

1
2
3
4
5
6
7
8
9
10
11
12
13
14
not_leap:
invoke printf, addr fostr2
jmp _end

is_leap:
invoke printf, addr fostr1

_end:
add esp, 8
mov esp, ebp
pop ebp

mov eax, 0
ret

not_leap标签处,调用printf函数,打印字符串fostr2。然后跳转到_end标签处。在is_leap标签处,调用printf函数,打印字符串fostr1。然后继续执行到_end标签处。

_end标签处,释放在堆栈上分配的8字节空间,恢复堆栈指针,并将旧的基址寄存器值恢复。最后,将eax寄存器设置为0并返回。

整个代码段的目的是读取用户输入的一个整数,然后判断该整数是否为闰年,并根据判断结果打印相应的字符串。

实验三:循环程序设计实验

1. 2012-2100的闰年数量

参考代码
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
.586
.MODEL flat,stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

;include vcIO.inc

.data
Lyear dword 25 dup(0) ;结果数组
msg byte 'Leap year is follow:',10,0
format byte '%d',9,0

.code
main Proc
xor ecx,ecx ;将寄存器ECX清零,用于计数闰年的个数
mov esi,2012 ;将寄存器ESI的值设置为2012,即开始年份
jmp testing
body:
mov eax,esi ;将寄存器EAX的值设置为ESI,即当前年份
mov ebx,4 ;将寄存器EBX的值设置为4,用于进行除法运算
cdq
div ebx

cmp edx,0
jne next ;如果 year % 4 != 0,则跳转到标签`next`处
;此处已满足被4整除

mov eax,esi
mov ebx,100
cdq
div ebx

cmp edx,0
je next ;如果 year % 4 == 100,则跳转至标签`next`处

;此处已满足被4整除且不被100整除

mov dword ptr Lyear[ecx*4],esi
;保存结果,乘4因为32位整数四字节大小

inc ecx ;增加计数
jmp over

next: ;此处判断是否被400整除
mov eax,esi
mov ebx,400
cdq
div ebx
cmp edx,0
jne over

mov dword ptr Lyear[ecx*4],esi
inc ecx
over:
inc esi ;增加年份计数
testing:
cmp esi,2100
jl body

pushad ;保护寄存器现场
invoke printf,offset msg
popad

xor esi,esi
again:
pushad
invoke printf,offset format,dword ptr Lyear[esi*4]
popad
inc esi
loop again

invoke ExitProcess, 0
main endp
end main

2. 回文串判断

“回文串”是一个正读和反读都一样的字符串,比如“eye”、“level”、“noon”等。请写一个程序测试一字符串是否是“回文”, 是“回文”则显示“Y”。

提示:

  1. 合理分配寄存器: left = esi, right = edi, flag = al = '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
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
INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

.386
.model flat,stdcall

ExitProcess PROTO,
dwExitCode:DWORD

printf PROTO C : dword,:vararg
scanf PROTO C : dword,:vararg

;INCLUDE vcIO.inc
.data
array BYTE "lwasdffdsawl",0
str_error BYTE "该字符串不是回文串",0ah,0
str_ok BYTE "该字符串是回文串",0ah,0
.code
main PROC
xor esi, esi ;esi为头指针
mov edi, LENGTHOF array - 2 ;edi为尾指针
;数组索引从0 ~ array-1,其中最后一位为 '\0'
;故实际最后一位的索引为 array-2

jmp TESTING
FORLOOP:
mov al,array[esi*1]
mov bl,array[edi*1]

cmp al,bl
jne EXIT ;如果头尾指针元素不同则不是回文串

inc esi
dec edi
jmp TESTING

EXIT:
invoke printf, OFFSET str_error
jmp ENDLOOP

TESTING:
cmp esi,edi
jbe FORLOOP ;esi<=edi 时继续循环

invoke printf, OFFSET str_ok

ENDLOOP:
invoke ExitProcess, 0
main ENDP
END main

3. 字符统计

编程写一个完整的程序统计字符串msg中的空格的个数与小写字母的个数,并分别将它们存入space单元与char单元中并输出。

参考代码
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
.586
.MODEL flat, stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

.data
msg byte "hello, m a s m!", 10, 0
space dword ?
lowercase dword ?
frmStr byte "space num = %d, lowercase num = %d", 10, 0

.code
main proc
mov ecx, lengthof msg
; 将字符串 msg 的长度存储在 ECX 寄存器中
mov esi, offset msg
; 将字符串 msg 的起始地址存储在 ESI 寄存器中
sub eax, eax
; 将 EAX 寄存器清零,用于计数空格字符的数量
mov ebx, eax
; 将 EBX 寄存器清零,用于计数小写字母的数量

again:
mov dl, [esi] ; 将当前字符读取到 DL 寄存器中
cmp dl, ' ' ; 比较当前字符与空格字符 ' ' 是否相等
jnz letter ; 如果不相等,跳转到 letter 标签继续执行
inc eax ; 空格字符计数器加 1
letter:
cmp dl, 'a' ; 比较当前字符与小写字母 'a' 是否相等
jb next ; 如果当前字符小于 'a',跳转到 next 标签
cmp dl, 'z' ; 比较当前字符与小写字母 'z' 是否相等
jg next ; 如果当前字符大于 'z',跳转到 next 标签
inc ebx ; 小写字母计数器加 1
next:
inc esi ; 字符串指针增加 1
loop again ; 继续循环,直到 ECX 寄存器减为零

pushad ; 保存所有通用寄存器的值
invoke printf, offset frmStr, eax, ebx
; 调用 printf 函数,输出格式化字符串,并传递计数器的值

popad ; 恢复之前保存的通用寄存器的值
invoke ExitProcess, 0
main endp
end main

4. 求数组最大值与最小值

编程写一个完整的程序,求数组 array[12, 4, 168, 122, -33, 56, 78, 99, 345, 66, -5] 中的最大值与最小值,并将它们分别存入 maxmin 单元中。

参考代码
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
.586
.MODEL flat, stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode: DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

; include vcIO.inc
_getch PROTO C: DWORD, : vararg
.data
array dword 12, 4, 168, 122, -33, 56, 78, 99, 345, 66, -5
fstr1 byte 'min=%d', 0ah, 0
fstr2 byte 'max=%d', 0ah, 0
.code
main proc
mov edx, offset array ;索引
mov ecx, lengthof array ;计数器

mov eax, [edx] ;min
mov ebx, [edx] ;max
xor esi, esi ;temp变量


_loop:
mov esi, [edx]

check_min:
cmp eax, esi
jle check_max
mov eax, esi

check_max:
cmp ebx, esi
jge check_end
mov ebx, esi


check_end:
add edx, 4
loop _loop


invoke printf, addr fstr1, eax
invoke printf, addr fstr2, ebx

invoke ExitProcess, 0
main endp
end main

5. 寻找回文数

求10到10000之间所有回文数并输出。要求每行输出10个数。

参考代码
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
.586
.MODEL flat,stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

.data
msg1 byte '%d',9,0
msg2 byte 10,0


.code
main Proc
mov esi,10 ; esi 为起始检查值
mov edi,0; edi 为输出个数计数器


body:
mov ecx,0 ; ecx 存放反转后的当前 esi
mov eax,esi

reverse:
imul ecx, 10
mov ebx, 10
cdq
idiv ebx
add ecx, edx

cmp eax, 0
jne reverse

cmp ecx, esi
jne loop_check

invoke printf,addr msg1, ecx ;打印回文数

inc edi
cmp edi, 10
jne loop_check

invoke printf, addr msg2 ;打印换行符
xor edi, edi ;edi 置0


loop_check:
inc esi
cmp esi,10000
jl body

invoke ExitProcess, 0
main endp
end main

6. 剔除空格

有一个首地址为 string 的字符串,剔除 string中所有的空格字符。请从字符串最后一个字符开始逐个向前判断、并进行处理。

参考代码
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
.586
.MODEL flat, stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib
INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

.386
.model flat, stdcall

ExitProcess PROTO,
dwExitCode: DWORD

printf PROTO C: DWORD, : vararg
scanf PROTO C: DWORD, : vararg

.data
string byte 'Dubi Dubi Duba Dubi Dubi Duba Perry', 0
format byte '%s', 10, 0

.code
main Proc
mov esi, lengthof string - 1 ; esi == scanP
mov edi, esi ; edi == tailP
jmp body_check
body:
cmp byte ptr string[esi], ' '
jne _continue

mov ebx, esi ; ebx == moveP
jmp move_check

_move:
mov al, byte ptr string[ebx + 1]
mov byte ptr string[ebx], al
inc ebx

move_check:
cmp ebx, edi
jl _move

dec edi
_continue:
dec esi
body_check:
cmp esi, 0
jge body

invoke printf, offset format, offset string

invoke ExitProcess, 0
main endp
end main

这段代码简单来说就是从后往前寻找空白字符,如果找到了,将后续所有往前移动一步然后接着找空白字符,直至找到字符串的头部。

7. 素数筛

求出2~1000之间的所有素数,并将它们存入 Prime 数组中,素数的个数存入变量 Pcounter 中。要求每行输出10个数。

参考代码
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
.586
.MODEL flat, stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode: DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

.data
fstr1 byte '%d ', 0
fstr2 byte 10, 0
fstr3 byte 'prime number count: %d', 10, 0
prime dword 2, 3 , 0 dup(1000)

.code
main Proc
mov esi, 4 ;start number
mov ecx, 2 ;prime counter
jmp body_check

body:
mov edi, 0 ;素数筛索引

sieve:
mov eax, esi
mov ebx, dword ptr prime[edi*4]
cdq
idiv ebx
cmp edx, 0
jz _continue ;可被整除,跳至_continue


inc edi
cmp edi, ecx
jl sieve

; 未被筛除,将其加入素数筛中
mov dword ptr prime[ecx*4], esi
inc ecx

_continue:
inc esi ;移动至下一个待筛数字
body_check:
cmp esi, 1000
jl body




mov eax, 0 ;prime计数器
mov esi, 0 ;格式化计数器
print:
mov ebx, dword ptr prime[eax*4]

pushad
invoke printf, addr fstr1, ebx
popad

inc eax
inc esi
cmp esi, 10

jne _continue_print

pushad
invoke printf, addr fstr2
popad

mov esi, 0
_continue_print:
loop print

invoke ExitProcess, 0
main endp
end main

实验四:子程序与操作系统功能调用

1. 递归阶乘

编写一个求 \(n!\) 的子程序,利用它求 \(1!+2! +3! +4! +5! +6! +7! +8!\) 的和(46233)并输出。

参考代码
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
INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

.386
.model flat, stdcall

ExitProcess PROTO,
dwExitCode: DWORD

printf PROTO C: DWORD, : VARARG

.data
msg1 byte "f(x)=x!",10,0
msg2 byte "f(%d)=%d",10, 0
msg3 byte "sum=%d", 10,0

.code

main PROC
invoke printf, addr msg1

mov ecx, 1
mov edx, 0

fac:
push ecx
call Factorial
add esp, 4
add edx, eax

pushad
invoke printf, addr msg2, ecx, eax
popad


inc ecx
cmp ecx, 8
jle fac

invoke printf, addr msg3, edx

invoke ExitProcess, 0
main ENDP

Factorial PROC
push ebp
mov ebp, esp
sub esp, 4 ; 为局部变量分配4字节的栈空间

mov eax, [ebp + 8] ; 获取参数n的值
cmp eax, 1
jbe Return ; 如果n小于等于1,直接返回1

dec eax ; 将n减1
push eax ; 将n-1作为参数压入栈中
call Factorial ; 递归调用阶乘子过程
add esp, 4 ; 清除栈中的参数

imul eax, [ebp + 8] ; 将结果乘以n,存储在eax中

Return:
mov esp, ebp
pop ebp
ret 4 ; 从过程返回并清除栈中的参数
Factorial ENDP

END main

2. 斐波那契数列

Fibonacci numbers的定义: \[ f_1=1,\ f_2=1,\ f_n = f_{n-1}+f_{n-2}\ when \ n\ge3 \] 编程输出 Fibonacci numbers 的前20项。 思考题:在不产生溢出的情况下 \(n\) 的最大值是多少?

参考代码
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
INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

.386
.model flat, stdcall

ExitProcess PROTO,
dwExitCode:DWORD

printf PROTO C : dword, :vararg
scanf PROTO C : dword, :vararg

.data
array dword 1, 1, 18 dup(0)
format byte '%d', 10, 0

.code
main Proc
mov esi, 2
jmp check

body:
mov eax, dword ptr array[esi*4 - 4]
mov ebx, dword ptr array[esi*4 - 8]
add eax, ebx
mov dword ptr array[esi*4], eax
inc esi

check:
cmp esi, 20
jl body


xor esi, esi
mov ecx, 20
print:
pushad
invoke printf, offset format, dword ptr array[esi]
popad
add esi, 4
loop print

invoke ExitProcess, 0
main endp
end main

思考题答案
1
2
3
4
5
6
7
8
9
10
11
const max = 2**32 - 1
let s = Array(100).fill(1)
s[0]=0
for(let i = 3;;i++){
s[i]=s[i-1]+s[i-2]
if(s[i] > max) {
console.log("i=", i - 1)
console.log("max=", s[i -1])
break
}
}

\[i = 47, max = 2971215073\]

3. 欧几里得算法

编程写一个名为 Gcd 的求两个数最大公约数子程序,主子程序间的参数传递通过堆栈完成。调用 Gcd 子程序求出三个双自变量:dvar1dvar2dvar3 的最大公约数并输出。

提示:\(result=gcd(gcd(dvar1,dvar2), dvar3)\)

参考代码
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
.586
.MODEL flat, stdcall

includelib msvcrt.lib

INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

ExitProcess PROTO, dwExitCode:DWORD ; return code
printf proto C: vararg
scanf proto C: vararg

;include vcIO.inc
.data
prompt byte '请输入三个整数(以空格分隔):', 0
fmt1 byte '%d %d %d', 0
fmt2 byte '最大公约数:%d', 13, 10, 13, 10, 0
dvar1 dword ?
dvar2 dword ?
dvar3 dword ?
res dword ?

.code
main proc


invoke printf, offset prompt
invoke scanf, offset fmt1, offset dvar1, offset dvar2, offset dvar3

push dvar1
push dvar2
call gcd ; 堆栈传参,结果保存在 EAX
add esp, 8
push eax
push dvar3
call gcd
add esp, 8
mov res, eax
pushad
invoke printf, offset fmt2, res
popad

invoke ExitProcess, 0
main endp

gcd proc
push ebp
mov ebp, esp ; 保存旧栈帧,建立新栈帧

push ebx
push edx
mov eax, [ebp + 8]
mov ebx, [ebp + 12]

.if eax < ebx
xchg eax, ebx
.endif

.while ebx != 0
xor edx, edx
idiv ebx
mov eax, edx
xchg eax, ebx
.endw

pop edx
pop ebx
pop ebp
ret
gcd endp
end main

代码解释

在提供的代码中,欧几里得算法的实现部分位于gcd procgcd endp之间的代码块中。让我们逐行详细解释这部分代码的实现过程:

1
2
3
4
5
6
7
8
gcd proc
push ebp
mov ebp, esp ; 保存旧栈帧,建立新栈帧

push ebx
push edx
mov eax, [ebp + 8]
mov ebx, [ebp + 12]

首先,过程开始时,保存旧的栈帧指针(ebp)并将当前栈帧指针(esp)赋给ebp,以建立新的栈帧。然后,将ebx和edx寄存器的值入栈保存。接下来,将第一个参数(dvar1)的值加载到eax寄存器,将第二个参数(dvar2)的值加载到ebx寄存器。

1
2
3
.if eax < ebx
xchg eax, ebx
.endif

通过使用xchg指令,将eax和ebx的值交换,以确保eax中存储的值始终大于等于ebx中存储的值。

1
2
3
4
5
6
.while ebx != 0
xor edx, edx
idiv ebx
mov eax, edx
xchg eax, ebx
.endw

接下来,使用循环来执行欧几里得算法的主要部分。使用while循环,条件是ebx不等于0。

  • 首先,通过使用xor指令将edx寄存器清零,以准备执行idiv(除法)操作。

  • 然后,使用idiv指令将eax寄存器的值除以ebx寄存器的值。idiv指令执行有符号除法,并将商存储在eax中,余数存储在edx中。

  • 将edx的值移动到eax寄存器,更新eax的值为余数。

  • 最后,再次使用xchg指令交换eax和ebx的值。

这个循环将一直执行,直到ebx等于0为止,此时最大公约数存储在eax寄存器中。

1
2
3
4
5
    pop edx
pop ebx
pop ebp
ret
gcd endp

最后,恢复先前保存的ebx和edx的值,然后恢复旧的栈帧指针ebp。通过ret指令,从gcd过程返回到调用它的位置。

在主过程中,使用call gcd的方式调用gcd过程,将两个整数作为参数传递给它。然后,使用push eax将最大公约数的值压入堆栈,以便后续处理。

这样,在主过程中调用两次gcd过程,分别计算了dvar1和dvar2、dvar3和之前计算得到的最大公约数的最大公约数。最后,将最终的最大公约数(res)存储在变量中,并使用printf函数将其输出到控制台。

4. 冒泡排序

编程写一个名为 Bubble 的冒泡排序子程序,主子程序间的参数传递通过堆栈完成;并写主程序验证它(array dword 10,1,8,7,5,3,4,6,2,9)。

参考代码
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
INCLUDELIB kernel32.lib
INCLUDELIB ucrt.lib
INCLUDELIB legacy_stdio_definitions.lib

.386
.model flat, stdcall

ExitProcess PROTO,
dwExitCode: DWORD

printf PROTO C: DWORD, :vararg
scanf PROTO C: DWORD, :vararg

.data
array dword 10, 1, 8, 7, 5, 3, 4, 6, 2, 9
msg byte '%d ', 0

.code

main Proc
mov ecx, lengthof array - 1
mov esi, offset array
push ecx
push esi
call BubbleSort


mov ecx, lengthof array
mov esi, 0
print:
pushad
invoke printf, offset msg, dword ptr array[esi * 4]
popad
inc esi
loop print

invoke ExitProcess, 0
main endp

BubbleSort proc
push ebp
mov ebp, esp ;建立栈帧

mov edx, dword ptr [ebp + 8] ;数组地址
mov ecx, dword ptr [ebp + 12] ;数组末位元素索引

_out: ;外层循环
push ecx
xor esi, esi

_in:
mov eax, [edx + esi * 4]
mov ebx, [edx + esi * 4 + 4]
cmp eax, ebx

jle _continue
mov [edx + esi * 4], ebx
mov [edx + esi * 4 + 4], eax

_continue:
inc esi
loop _in

pop ecx ;保护 ecx,使得外层循环必定执行 ecx 次
loop _out

pop ebp ;还原栈帧
ret 8
BubbleSort endp
end main

微机总线

总线是微机系统中用于传输信息的公共通道,部件通过总线相互连接实现数据传输,部件之间需要遵守共同的总线规范。

总线技术

总线类型

芯片总线

  1. 集成电路芯片内部,芯片级互联,或者系统中各种不同的器件连接起来的总线。
  2. 局部总线,微处理器的引脚信号。
  3. 片内总线,大规模集成电路内部连接。

内总线

  1. 模块级互联,主机内部功能单元互连的总线。
  2. 板级总线、母板总线,或系统总线
  3. 系统总线(System Bus)是微机系统的主要总线
  4. 内部总线从一条变为多条,形成多总线结构

外总线(External Bus)

  1. 设备级互连,微机与其外设或微机之间连接的总线
  2. 过去,指通信总线
  3. 现在,常延伸为外设总线
微机总线层次结构

总线的数据传输

主设备(Master):控制总线完成数据传输 从设备(Slave):被动完成数据交换

同一时刻最多一个主设备控制总线,从设备无限制 同一时刻最多一设备通过总线发送数据,接受设备数量无限制

总线的操作

请求和仲裁(Bus request & Arbitration)

  1. 使用总线的主模块提出申请
  2. 仲裁机制把总线分配给请求设备

寻址(Addressing)

主模块发出访问的从模块地址信息以及有关命令,启动从模块。

数据传送 (Data Transfer)

源模块通过数据总线向目标模块发送信息。

结束(Ending)

数据、地址、状态、命令信息均从总线上撤除,让出总线

仲裁

总线仲裁:决定当前控制总线的主设备

集中仲裁

  1. 系统具有中央仲裁器(控制器)
  2. 负责主模块的总线请求和分配总线的使用
  3. 主模块有两条信号线:总线请求和总线响应

分布仲裁

  1. 各个主模块都有自己的仲裁器和唯一的仲裁号
  2. 主模块请求总线时,发送其仲裁号
  3. 比较各个主设备仲裁号决定

同步方式

同步时序

  1. 总线操作过程由共用的总线时钟信号控制
  2. 适合速度相当的器件互连总线,否则需要准备好信号让快速器件等待慢速器件(半同步)
  3. 半同步时序需要增加READY等状态信号
  4. 处理器控制的总线时序采用同步时序

异步时序

  1. 总线操作需要握手联络(应答)信号控制
  2. 传输的开始伴随有启动(选通或读写)信号
  3. 传输的结束有一个确认信号,进行应答
  4. 操作周期可变、可以混合慢速和快速器件

传输类型

猝发传送(数据块传送)

给出起始地址,将固定块长的数据,一个就一个的从相邻地址读入或写出

写后读(Read-After-Write)

先写后读,适用于校验。

读修改写 (Read-Modify-Write)

先读后写同一个地址单元,适用共享数据保护

广播 (Broadcast)

一个主设备对多个从设备的写入操作

性能指标

总线宽度(8,16,32,64位)

总线能够同时传送的数据位数 位数越多,一次能够传送的数据量越大

总线频率Hz

总线信号的时钟频率 时钟频率越高,工作速度越快

总线带宽(Bandwidth)

单位时间传输的数据量MB/s 总线带宽越大,总线性能越高

\(总线带宽=总线传输速率=吞吐率=传输的数据量\div所需时间\)

计算例子

总线信号和时序

总线分类

地址总线
  1. 主从模块的地址总线输出
  2. 从模块的地址总线输入
数据总线

双向传输

控制总线
  1. 有输出也有输入信号
  2. 基本功能是控制存储器及I/O读写操作
  3. 还包括中断与DMA控制、总线仲裁、数据传输握手联络等

引脚信号

引脚信号由以下方面构成

  1. 信号的功能
  2. 信号的流向
  3. 有效方式
  4. 三态能力
引脚信号功能示意图

总线时序

总线时序描述了总线信号的时间变化规律和相互关系。一个时钟周期是处理器的基本工作节拍。

8086芯片

8086的引脚信号

8086的总线时序

8086的基本总线周期由四个时钟周期 \(T_{1-4}\) 构成,存储器读和IO读,存储器写和IO写又可以分别统一到一个时序图表达。

写总线周期

写总线周期时序图1
写总线周期图2
Tw状态

读总线周期

读总线周期图1
读读总线周期时序图2

中断请求和响应、总线请求和响应、复位、时钟等信号的作用

在8086处理器的总线系统中,以下是各个信号的作用:

  1. 中断请求和响应:
    • 中断请求(INTR):外部设备通过将INTR引脚置为低电平来请求中断。设备可以向处理器发出中断请求,以便在需要时暂停当前正在执行的程序。
    • 中断响应(INTA):处理器在接收到中断请求后,通过将INTA引脚置为低电平来响应中断请求。这向外部设备发出确认信号,表明处理器已经准备好接收中断向量。
  2. 总线请求和响应:
    • 总线请求(BUSRQ):用于暂停处理器的总线操作,例如DMA控制器请求访问系统总线。
    • 总线响应(BUSD):用于确认处理器的总线请求,以便处理器知道其他设备已经释放了系统总线。
  3. 复位信号(RESET):
    • 复位信号用于将处理器重置为初始状态。当RESET引脚被拉低时,处理器会停止执行并重新初始化其内部状态。这是一个系统级信号,用于启动系统的初始化过程。
  4. 时钟信号(CLK):
    • 时钟信号提供处理器的基准时钟脉冲。它驱动处理器的内部时序和操作,并确保各个部件在正确的时间进行操作。处理器根据时钟信号的上升沿或下降沿执行指令和其他操作。

总的来说,中断请求和响应信号用于处理外部设备的中断,使处理器能够在需要时响应外部事件。总线请求和响应信号用于处理器和其他设备之间的总线控制和共享。复位信号用于将处理器重置为初始状态,而时钟信号则提供处理器操作的基准时序。这些信号在处理器的正常操作和系统的协调中起着重要的作用。

总结

总结1 总结2

输入输出接口

IO接口典型模式

内部结构

IO接口典型结构图

数据寄存器

保存处理器与外甥间交换的数据的寄存器。

状态寄存器

保存外设或其接口电路当前的工作状态信息

控制寄存器

保存控制接口电路和外设操作的有关信息。

基本功能

  1. 数据缓冲
  2. 信号变换

IO端口的编址

端口与存储器地址独立编址

独立两个地址空间。

优点:不占用存储器空寂

缺点:指令功能简单,寻址方式不如存储器指令丰富

统一编址

统一编排,共享地址空间,将IO地址映射到了存储器空间。

优点:不用专门设计IO指令和引脚,IO访问可复用存储器指令。

缺点:IO端口占据了存储器的地址空间。

IO地址译码

IO地址译码

输入输出指令

输入指令 IN 和输出指令 OUT

IO寻址方式

直接寻址

由IO指令直接提供8位IO地址,只能寻址最低256个地址。

在IO指令中,用i8表示这个地址,形式类似于立即数,但是在IO指令上就表示直接寻址的IO地址。

DX间接寻址

用16位寄存器DX保存IO地址。

IA-32处理器的IO地址共64k,每个地址对应一个8位端口,无需分段管理,最低256个地址可直接寻址或间接寻址访问,其余地址只能通过DX间接访问。

数据传输量

数据大小 寄存器
8位 AL
16位 AX
32位 EAX

无条件传送和查询传送

数据传送方式

软件控制的数据传送

无条件传送

字面意思,根据原文意思可能是指无阻塞的传送。

例如数码管、按键、LED灯总是处于就绪状态的设备可采用无条件传送,数据缓冲通过三台缓冲器和锁存器实现。

查询传送

询问外设工作状态并在外设准备好数据后开始数据传送。

特点
流程图
编程
查询输入
查询输出
中断传送

外设向处理器发送请求,处理器在满足条件的情况下中断暂停当前程序并与外设进行数据传送。

工作流程图

附加硬件控制的数据传送

DMA传送

需要大量数据传送的外设,处理器转移控制权给DMA控制器,由其控制外设与存储器的数据传送。

IO处理器控制传送

专门的IO处理器管理。


微机原理学习笔记
https://ooj2003.github.io/2023/03/14/微机原理学习笔记/
作者
OOJ2003
发布于
2023年3月14日
更新于
2023年9月17日
许可协议