前言

本博客用于复习IA32架构下微软宏汇编(MASM)的基础知识。作为初学者,本人水平有限,若内容存在疏漏或错误,恳请读者斧正

开发环境

使用Visual Studio 2017 Community

image-20250601144842836

image-20250601144907897

image-20250601144956515

image-20250601145040078

image-20250601145136031

程序格式

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
.386 ;使用.386指令集

.model flat,stdcall ;flat内存模式 调用子程序使用stdcall

comment/*
flat:
所有段(代码段、数据段、堆栈段)都使用相同的32位地址空间
不需要使用段寄存器(CS, DS, SS等),所有内存访问都使用32位偏移地址
stdcall:
参数从右向左压入堆栈
被调用函数负责清理堆栈(与cdecl不同,cdecl是调用者清理堆栈)
*/

option casemap:none ;大小写敏感

.const ;常量区
fm db "%s"

ARR_LEN = 1 ;也可以这样声明常量

.data ;全局变量区
x db 0 ;1bytes
y dw 0 ;字;2bytes
z dd 0 ;双字;4bytes
arr dd 50 dup(0) ;50个连续的值为零的4字节空间

.code
main proc ;main程序段
_start::

main endp
end _start ;end后的部分就是程序入口,在这里,_start就是程序入口

常用寄存器

EAX

通用寄存器之一,存放函数的返回值

EBX,ECX,EDX寄存器布局与此类似

image-20250601151224193

EBX

通用寄存器之一

ECX

通用寄存器之一,存放循环次数

也用于字符串操作(如 rep stosb)、移位指令(如 shl eax, cl`)

EDX

通用寄存器之一

ESI

通用寄存器之一

在字符串/内存操作中默认指向源数据地址

EDI

在字符串/内存操作中默认指向目标地址

ESP

栈顶指针寄存器,存放栈顶的地址

PUSH会先递减 ESP,再写入数据;POP 会读取数据后递增 ESP

EBP

通常用作栈帧基址(函数内通过 [EBP+offset] 访问局部变量和参数)

EIP

程序计数器寄存器,存放下一条指令的地址

常用标志位

CF(Carry Flag)

无符号数溢出标志

add/sub存在进位/借位时,CF置为1

ZF(Zero Flag)

运算结果为0时,置为1

SF(Sign Flag)

运算结果的最高位 0:正数 1:负数

PF(Parity Flag)

结果低8位的 1 的个数是否为偶数

0:奇校验

1:偶校验

OF(Overflow Flag)

有符号数溢出标志

DF(Direction Flag)

字符串操作方向标志

0:正向

1:反向

1
cld ;重置df为0

常用指令

inc-自增

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
;对全局变量自增
.data ;全局变量
x dd 0 ;x=0

.code
main proc ;main程序段
_start::
inc x ;x = 1

xor eax,eax
ret
main endp
end _start ;end后的部分就是程序入口,在这里,_start就是程序入口

;------------------------------------------------------

.data ;全局变量

.code
main proc ;main程序段
_start::
mov eax,0 ;eax:0x00000000
inc eax ;eax:0x00000001

xor eax,eax
ret
main endp
end _start ;end后的部分就是程序入口,在这里,_start就是程序入口

dec-自减

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
;对全局变量自减
.data ;全局变量
x dd 1 ;x=1

.code
main proc ;main程序段
_start::
dec x ;x = 0

xor eax,eax
ret
main endp
end _start ;end后的部分就是程序入口,在这里,_start就是程序入口

;------------------------------------------------------

.data ;全局变量

.code
main proc ;main程序段
_start::
mov eax,1 ;eax:0x00000001
dec eax ;eax:0x00000000

xor eax,eax
ret
main endp
end _start ;end后的部分就是程序入口,在这里,_start就是程序入口

mov

1
2
3
4
5
6
7
8
9
10
mov dst,src
;将立即数传送到寄存器
mov eax,1
;将寄存器中的值传送到另一寄存器;注意两个寄存器的大小要一致(8bits->8bits;16bits->16bits;32bits->32bits)
mov eax,ebx
mov al,ah
;mov的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数
mov [esp+1],[esp+2] ;错误
mov [esp+1],1 ;错误
mov x,y ;错误

movzx-零拓展传送

高位补零,无论源操作数的符号位(最高位)是 0 还是 1,高位全部填充 0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.code
main proc
_start::
mov al,-1 ;eax:??????ff

movzx ax,al ;al拓展到ax eax:????00ff

movzx,eax,al ;al扩展到eax eax:000000ff

movzx eax,ax ;ax拓展到eax

xor eax,eax
ret
main endp
end _start

初始的al

image-20250603182320971

movzx ax,al后(注意ah的变化)

image-20250603182329581

movzx eax,ax后

image-20250603182352218

movsx-符号拓展传送

源操作数高位是1就补1,高位是0就补0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.code
main proc
_start::
mov al,-1

movsx ax,al ;al拓展到ax eax:????ffff
movsx eax,al ;al拓展到eax eax:ffffffff

movsx eax,ax ;ax拓展到eax

xor eax,eax
ret
main endp
end _start

初始的al

image-20250603182637465

movsx ax,al后

image-20250603182705835

mov ax,eax后

image-20250603182721954

sub

1
2
3
4
5
6
7
8
9
10
11
12
13
sub dst,src

;寄存器中的值减去立即数
sub eax,1
;将寄存器的值减去另一寄存器中的值
sub eax,ebx
;将内存中的值减去寄存器中的值
sub x,ebx
sub [esi+4],eax
;sub的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数
sub [esp+1],[esp+2] ;错误
sub [esp+1],1 ;错误
sub x,y ;错误

add

1
2
3
4
5
6
7
8
9
10
11
12
add dst,src
;将立即数加到寄存器中
add eax,1
;将寄存器的值加到寄存器中
add eax,ebx
;将寄存器中的值加到内存
add x,ebx
add [esi+4],eax
;add的两个操作数不能都是内存操作数(比如.data段中的数据)或立即数
add [esp+1],[esp+2] ;错误
add [esp+1],1 ;错误
add x,y ;错误

adc-带进位的加法

cf标志位:

加法(ADD/ADC:如果运算结果的最高位产生进位,CF = 1,否则 CF = 0

减法(SUB/SBB:如果运算需要借位(被减数 < 减数),CF = 1,否则 CF = 0

移位/循环指令(SHL/SHR/ROL/ROR等):存放被移出的位。

比较指令(CMP:同减法,CMP A, B 等价于 SUB A, B(但不保存结果,只改标志位)

1
2
add sum,ebx	;sum的前4个字节
adc sum+4,0 ;给sum的后四个字节加上进位 <-> 相当于sum+4 = sum+4+0+cf

mul-无符号数乘法

隐含使用 AL/AX/EAX 作为被乘数

1
2
3
4
5
mul bl ;结果位于ax

mul bx ;结果:DX:AX (高16:低16)

mul ebx ;结果:EDX:EAX (高32:低32)

imul-有符号数乘法

单操作数

mul相同

双操作数

1
imul dest,src

dest=dest*src

dest只能是16位或32位寄存器,src可以是通用寄存器/内存操作数/立即数

三操作数

1
imul dest,src1,src2

dest = src1*src2

dest只能是16位或32位寄存器,src1可以是通用寄存器和内存,不能是立即数,src2只能是立即数

div-无符号整数除法

1
div oprd

oprd可以是通用寄存器和内存操作数,但不能是立即数

8位:

被除数:ax,商:al,余数ah

16位:

被除数:eax,商:ax,余数dx

32位:

被除数:edx:eax,商:eax,余数edx

移位指令

移位位数放在cl或者8位立即数

shl逻辑左移,sal算术左移

这俩其实是一条机器指令,只是方便记忆变成两条

1
2
3
4
SHL r/m, imm8   ; 左移立即数位
SHL r/m, CL ; 左移CL寄存器指定的位数
SAL r/m, imm8 ; 右移立即数位
SAL r/m, CL ; 右移CL寄存器指定的位数

每左移一位,右边补一位0,移出的最高位放在cf

shr逻辑右移

1
2
SHR r/m, imm8   ; 右移立即数位
SHR r/m, CL ; 右移CL寄存器指定的位数

右移一位,左边补一位0,移出的最低位放在cf

sar算术右移

1
2
SAR r/m, imm8   ; 右移立即数位
SAR r/m, CL ; 右移CL寄存器指定的位数

右移一位,左边符号位不变,移出的最低位放在cf

符号扩展指令

CBW

将al中8位有符号数扩展到ax中

若al最高位为0,ah全补0,否则全补1

1
2
3
4
5
6
7
8
9
.data
x1 db 0fh
y1 db 0ffh

mov al,x1 ;eax:??????0f
cbw ;eax:????000f

mov al,y1 ;eax:??????ff
cbw ;eax:????ffff

CWD

将ax中16位有符号数扩展到dx:ax中

若ax最高位为0,dx全补0,否则全补1

1
2
3
4
5
6
7
8
9
.data
x1 dw 0000fh
y1 dw 0ffffh

mov ax,x1 ;eax:????000f edx:????????
cwd ;eax:????000f edx:????0000

mov ax,y1 ;eax:????ffff edx:????????
cwd ;eax:????ffff edx:????ffff

CDQ

eax扩展到eax:edx

若eax最高位为0,edx全补0,否则全补1

1
2
3
4
5
6
7
8
9
.data
x1 dd 00000000fh
y1 dd 0f0000000h

mov eax,x1 ;eax:00000000f edx:????????
cdq ;eax:00000000f edx:00000000

mov eax,y1 ;eax=f0000000 edx:????????
cdq ;eax=f0000000 edx:ffffffff

CWDE

ax扩展到eax

若ax最高位为0,eax高16位全补0,否则全补1

1
2
3
4
5
6
7
8
9
.data
x1 dw 0000fh
y1 dw 0ffffh

mov ax,x1 ;eax:????000f
cwde ;eax:0000000f

mov ax,y1 ;eax:????f000
cwde ;eax:fffff000

串操作指令

movsb

将esi指向地址的数据复制到edi指向的地址(一次1字节)

然后根据DF标志位自动递增或递减ESI和EDI:

  • DF=0时:ESI和EDI递增1

  • DF=1时:ESI和EDI递减1

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    .data
    str1 db "Hello World!",00h
    len dd $-str1
    str2 db 00h


    lea esi,str1
    lea edi,str2

    movsb ;str2:H
    ;与rep结合,一次移动一块
    mov ecx,len
    lea esi,str1
    lea edi,str2

    rep movsb

movsw

将esi指向地址的数据复制到edi指向的地址(1次2字节)

然后根据DF标志位自动递增或递减ESI和EDI:

  • DF=0时:ESI和EDI递增1
  • DF=1时:ESI和EDI递减1

movsd

将esi指向地址的数据复制到edi指向的地址(1次4字节)

然后根据DF标志位自动递增或递减ESI和EDI:

  • DF=0时:ESI和EDI递增1
  • DF=1时:ESI和EDI递减1

stosb

需要配合rep来实现对一块连续内存进行串填充,填充次数存放在ecx寄存器中,填充值放在al/ax/eax中,填充到edi指向的地址(涉及目的地 ,使用edi)

字符串操作方向由df标志位实现(正向:df=0 反向:df=1)

rep stosb/stosw之前应使用cld指令先重置df

stosw

stosd

lea

取地址并传送

1
2
3
4
5
6
7
.data
arr db 50 dub(0)
.code
main proc
lea esi,arr ;取arr的首地址传送到esi中
main endp
end main

等价于

1
2
3
4
5
6
7
.data
arr db 50 dub(0)
.code
main proc
mov esi,offset arr ;取arr的首地址传送到esi中 offset:通用偏移地址获取
main endp
end main

xchg

交换内存/寄存器的值

1
2
xchg op1,op2
;op1和op2中至少有一个是寄存器操作数

and

1
and dest,src

dest = dest & src

or

1
or dest,src

dest = dest | src

xor

1
xor dest,src

dest = dest xor src

test

与and类似,但是不送到dest中,仅影响标志位

1
test dest,src

转移指令

jmp

无条件转移

转移到标签

1
2
3
test1:
mov eax,offset test1
jmp test1

寄存器间接转移

1
2
3
test1:
mov eax,offset test1
jmp eax ;转移到eax中的地址

je/jz

当zf=0时转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.code
main proc
_start::
test1:
mov eax,1
mov ebx,1

cmp eax,ebx

jz test1 ;此时相当于死循环 写成je也是相同的效果

xor eax,eax
ret
main endp
end _start

ja/jnbe

无符号数比较,高于(不低于等于)时转移(cf=0且zf=0)

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
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

cmp ebx,eax

ja test1 ;跳转到test1
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

;------------------------
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

cmp eax,ebx

ja test1
jmp end_if ;跳转到程序结束

end_if:
xor eax,eax
ret
main endp
end _start

jb/jnae

无符号数比较,低于(不高于等于)时转移

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
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

cmp ebx,eax

jb test1
jmp end_if ;跳转到程序结束

end_if:
xor eax,eax
ret
main endp
end _start

;------------------------
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

cmp eax,ebx

jb test1 ;跳转到test1
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jo

溢出转移(OF=1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.data
c1 dd 07fffffffh ;int_max

.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

add eax,c1

jo test1
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jno

不溢出转移(OF=0)

js

为负转移(sf=1)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

add eax,c1 ;0xffffffff = -1 为负

js test1 ;转移到test1
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jns

为正转移(sf=0)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.code
main proc
_start::
test1:
mov eax,1
mov ebx,4

add ebx,c1 ;2=0x00000010

jns test1 ;转移到test1
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jg/jnle

有符号数比较,大于/不小于等于时转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.code
main proc
_start::
test1:
mov eax,-1
mov ebx,-4

cmp eax,ebx

jg test1 ;-1 > -4 转移
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jl/jnge

有符号数比较,小于/不大于等于时转移

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
.code
main proc
_start::
test1:
mov eax,-1
mov ebx,-4

cmp ebx,eax

jl test1 ;-4 < -1 转移
jmp end_if

end_if:
xor eax,eax
ret
main endp
end _start

jcxz

cx=0时转移

jecxz

ecx=0时转移

寻址方式

寄存器寻址

1
add eax,ebx

基址寻址

常用于堆栈

1
2
3
push ebp
mov ebp,esp
mov eax,[ebp+4] ;基址寻址

立即数寻址

1
mov eax,064h ;常量操作

变址寻址

1
mov eax,[esi+10h] ;数组操作

相对寻址

1
adc sum+4,0 ;数组操作

寄存器间接寻址

访问指针指向的变量

1
add dword ptr [ebx],ecx

顺序程序设计

eg:有三个长度分别为1、2、4个字节的数据,编写程序求和存放到内存中

程序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
.data ;全局变量
a db 1
b dw 2
c1 dd 0ffffffffh ;无符号数
sum dd 0,0 ;因为要考虑进位存储,需要额外再开4字节空间

.code
main proc
_start::
movzx ax,a ;a零拓展到ax

add ax,b ;b加到ax中

movzx eax,ax ;ax拓展到eax中

add eax,c1 ;发生溢出,CF标志位被置为1

mov sum,eax

adc sum+4,0 ;进位传送到高4字节 相当于sum+4 = sum+4+0+CF

xor eax,eax
ret
main endp
end _start

程序2

三个数据均为有符号数,求和的结果不考虑进位的存储(进位直接丢掉)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.data ;全局变量
a db 1
b dw 2
c1 dd 0ffffffffh ;int:-1 uint:最大值
sum dd 0

.code
main proc
_start::
movsx ax,a ;a有符号拓展到ax

add ax,b ;b加到ax中

movsx eax,ax ;ax有符号拓展到eax中

add eax,c1 ;2

xor eax,eax
ret
main endp
end _start

思考

1.用户如何自定义超过系统事先定义好的数据类型的长度?(比如在C语言中实现128位整数)

使用连续的等长内存空间来分段存储,根据地址偏移进行分段运算和进位传递

1
2
_int64 arr[2]; //arr[0]:低64位	arr[1]:高64位
_int32 arr[4];

2.不同寻址方式对编写程序的作用

作用:不同的寻址方式有不同的程序执行效率,程序可读性与可维护性

简单分支程序设计

汇编中是通过条件/无条件转移指令实现简单的分支程序(if-else结构)

程序1

实现逻辑或的逻辑短路

对应的c语言代码

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int a=5,b=6,c=7,d=8,m=2,n=2;
(m=a<b)||(n=c>d);
printf("%d\t%d",m,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
.data
a dd 5
b dd 6
c1 dd 7
d dd 8
m dd 2
n dd 2

.code
main proc
_start::
mov eax,a

cmp eax,b

jl alb ;a小于b,exp1 || exp2中的exp1为true,短路

mov eax,0
mov m,eax

mov eax,c1
cmp eax,d
jle cleb ;给n赋值0

mov eax,1
mov n,eax ;给n赋值1
jmp end_proc

alb: ;a小于b,exp1 || exp2中的exp1为true,短路
mov eax,1
mov m,eax
jmp end_proc
cleb:
mov eax,0
mov n,eax
jmp end_proc

end_proc:
xor eax,eax
ret
main endp
end _start

程序2

实现逻辑与的逻辑短路

对应的c语言代码

1
2
3
4
5
6
7
#include <stdio.h>
int main()
{
int a=5,b=6,c=7,d=8,m=2,n=2;
(m=a<b)&&(n=c>d);
printf("%d\t%d",m,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
.data
a dd 5
b dd 6
c1 dd 7
d dd 8
m dd 2
n dd 2

.code
main proc
_start::
mov eax,a

cmp eax,b

jge ageb ;大于等于,第一个表达式为假,短路

mov eax,1
mov m,eax

mov eax,c1
cmp eax,d
jle cleb ;小于等于,第2个表达式为假

mov eax,1
mov n,eax
jmp end_proc

ageb:
mov eax,0
mov m,eax ;给m赋值为0
jmp end_proc
cleb:
mov eax,0
mov n,eax
jmp end_proc

end_proc:
xor eax,eax
ret
main endp
end _start

思考

1.简述分支语句的实现原理(注意标志位在其中的作用)

根据标志位值的变化和相应的跳转指令

2.简述逻辑运算短路的特征

逻辑或: exp1 || exp2

exp1结果为真时,整体就为真,不再执行exp2就直接跳转到对应分支

逻辑与 exp1 && exp2

exp1结果为假时,整体就为假,不再执行exp2就直接跳转到对应分支

地址表分支程序设计

在简单分支程序中,有太多的标签与各种各样的跳转指令,分支的不可预测性强,这样会破坏流水线技术

img

但是在地址表分支程序中,所有分支的地址都位于一个地址表中,其实只需要根据间接寻址,使用一条jmp指令就能转移到对应分支

原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 main()
{
int grade = 90;
switch (grade / 10)
{
case 9:
printf("excellence");
break;
case 8:
printf("good");
break;
case 7:
printf("average");
break;
case 6:
printf("pass");
break;
default:
printf("fail");
}
return 0;
}

汇编实现

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
.data
grade dd 90
adr_table dd offset case6,offset case7,offset case8,offset case9

.code
main proc
_start::
xor edx,edx

mov eax,grade
mov bx,10
div bx

sub eax,6 ;获取和6的差值,即后面的偏移量

cmp eax,0
jl default ;低于60

mov eax,[adr_table+eax*4] ;根据偏移量找到对应分支的地址,解引用后传送到eax中
jmp eax ;转移到对应分支

case6::
mov eax,6
case7::
mov eax,7
case8::
mov eax,8
case9::
mov eax,9
default:

end_proc:
xor eax,eax
ret
main endp
end _start

思考

1.采用地址表和不采用地址表有什么区别

地址表是一种空间换时间的算法,时间复杂度为O(1),但是空间复杂度达到了O(n)

通过连续内存来存储分支的地址,减少了程序中各种转移指令的使用,不仅使程序可读性更强,同时分支可预测,在分支之间转移的时间少,利于流水线模式

区别:

采用地址表:程序可读性更强,直接使用地址偏移量跳转,更加高效;但是空间开销较高,比较适合条件连续的分支

不采用地址表:程序可读性没那么好,容易逻辑混乱。但是空间开销较小

2.如果分支常量值不连续,还可以使用地址表吗

可以,没有值的地方填充0或者指定值(空间换时间)

循环程序设计

eg:

编写程序实现C语言函数void *memset(void* s,int ch,size_t n),将指定的内存中连续N个字节填写成指定的内容,要求:

  1. 每次填写一个字节

  2. 每次填写一个字

  3. 分别用LOOP指令、串操作指令、条件(无条件)转移指令分别实现以上的操作

loop指令实现

循环次数由ecx寄存器决定

1.每次填写一个字节

定义数据

1
2
3
4
fill_var = 03ch ;将填充值定义为常量
.data
arr db 10 dup(0) ;n dup(x) 给连续n个空间赋值x
arr_len dd $-arr ;$表示当前地址,$-arr表示填充的字节数有多少

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.code
main proc
_start::
mov al,fill_var ;一次写入一个字节,填充值传送到al中
mov ecx,arr_len ;循环次数传送到ecx中
lea esi,arr ;取arr的首地址传送到esi中
memset:
mov [esi],al ;将填充值传送到esi指向的地址,完成一字节的填充
inc esi ;指针向后移一个字节
loop memset ;循环

xor eax,eax
ret
main endp
end _start

2.每次填写一个字

定义数据

1
2
3
4
fill_var = 03ch ;填充值
.data
arr dw 10 dup(0) ;字数组
arr_len dd ($-arr)/2 ;填充字数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.code
main proc
_start::
mov ax,fill_var
mov ecx,arr_len
lea esi,arr
memset:
mov [esi],ax
add esi,2 ;因为一次写入一个字(2个字节),这里地址要加2
loop memset

xor eax,eax
ret
main endp
end _start

串操作指令实现

需要配合rep来实现对一块连续内存进行串填充,填充次数存放在ecx寄存器中,填充值放在al/ax/eax中,填充到edi指向的地址(涉及目的地 ,使用edi)

字符串操作方向由df标志位实现(正向:df=0 反向:df=1)

rep stosb/stosw之前应使用cld指令先重置df

stosb

将al中的值写入[edi]中,即一次写入一个字节

定义数据

1
2
3
4
fill_var = 03ch ;填充值
.data
arr db 10 dup(0) ;字节数组
arr_len dd ($-arr) ;填充字节数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.code
main proc
_start::
mov al,fill_var
mov ecx,arr_len ;重复次数
lea edi,arr ;操作的首地址一定要放在edi中

cld ;清空df位
rep stosb

xor eax,eax
ret
main endp
end _start

stosw

将ax中的值写入[edi]中,即一次写入一个字(2字节)

定义数据

1
2
3
4
fill_var = 03ch ;填充值
.data
arr dw 10 dup(0) ;字数组
arr_len dd ($-arr)/2 ;填充字数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
.code
main proc
_start::
mov ax,fill_var
mov ecx,arr_len
lea edi,arr

cld
rep stosw

xor eax,eax
ret
main endp
end _start

条件/无条件转移指令实现

类似与C语言中的

1
2
3
for(int i = len;i>=0;--i){
/*code*/
}

一次写入一个字节

定义数据

1
2
3
4
fill_var = 03ch ;填充值
.data
arr db 10 dup(0) ;字节数组
arr_len dd ($-arr) ;填充字节数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.code
main proc
_start::
mov al,fill_var
mov ecx,arr_len
lea esi,arr

memset:
cmp ecx,0
jle end_loop

dec ecx

mov [esi],al
inc esi

jmp memset

end_loop:
xor eax,eax
ret
main endp
end _start

一次写入一个字

定义数据

1
2
3
4
fill_var = 03ch ;填充值
.data
arr dw 10 dup(0) ;字数组
arr_len dd ($-arr)/2 ;填充字数

实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.code
main proc
_start::
mov ax,fill_var
mov ecx,arr_len
lea esi,arr

memset:
cmp ecx,0
jle end_loop

dec ecx

mov [esi],al
add esi,2

jmp memset

end_loop:
xor eax,eax
ret
main endp
end _start

实现冒泡排序

C语言实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
int k = n - 1;
for (int i = 0; i < n; ++i)
{
for (int j = 0; j < k; ++j)
{
if (arr[j] > arr[j + 1])
{
int tmp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = tmp;
}
}
k--;
}

关键点:

内外两层循环,内循环次数比外循环少一次

内循环中有一个临时变量用于交换值

汇编实现

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
fill_var = 03ch ;填充值
.data
arr db 10,9,8,7,6,5,4,3,2,1 ;字节数组
arr_len dd ($-arr) ;填充字节数

.code
main proc
_start::
lea esi,arr

mov ecx,arr_len

outer:
cmp ecx,0
jbe end_loop


push ecx ;内循环也需要ecx作为循环次数,因此需要压入堆栈保护起来
push esi ;esi指向首地址,但是内循环中需要esi向后移动,为了防止找不到首地址,也需要将esi压入堆栈保护
inner:
mov bl,[esi] ;用寄存器模拟临时变量
mov dl,[esi+1] ;用寄存器模拟临时变量

cmp bl,dl ;if (arr[j] > arr[j + 1])
ja swap
jmp no_swap
swap:
mov [esi],dl
mov [esi+1],bl

no_swap:
inc esi ;指向下一字节
loop inner

pop esi
pop ecx ;平衡堆栈
loop outer
end_loop:
xor eax,eax
ret
main endp
end _start

初始数组:

image-20250604171439997

排序后:

image-20250604171459827

思考

loop指令和串操作指令的性能对比,必须通过循环实现的程序如何提高性能?

​ 使用串操作指令的性能优于loop指令

​ 提高性能:

​ 1.使用更宽的数据进行操作,减少循环次数

​ 2.确保内存对齐

​ 3.避免使用loop,使用更高效的指令(比如串操作指令)

循环程序和分支程序的关系?

​ 循环程序和分支程序都依赖于条件判断和跳转指令,不同的是,循环程序是反复多次跳转,而分支程序跳转后变不再反复跳转回来

LOOP的双重循环例子,理解系统如何存储临时变量?

​ 将ecx压入堆栈中进行保护,防止内层循环改变ecx的值导致外层循环的结果错误

高级语言中break和continue的实现?

​ continue:跳转到当前循环的标签

​ break:跳转到循环外的另一个标签

子程序设计

格式

1
2
3
4
5
6
7
子程序名 proc
push ebp ;栈帧基址
mov ebp,esp

pop ebp ;平衡堆栈
ret
子程序名 endp

调用

1
2
call 子程序名
add esp,8 ;例子

call指令会将下一条指令(在这里就是add esp,8)的地址压入堆栈中(称为返回地址,子程序ret后就会返回到这个位置),进入子程序后,堆栈内部情况如下

image-20250604181127014

重点

能判断出堆栈的状态,可以用excel画当前堆栈示意图

调用约定

stdcall

参数从右至左压入堆栈,子程序(被调函数)负责平衡堆栈

1
int addxy(int x,int y);

在汇编中传入参数

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
.code
addxy proc
push ebp
mov ebp,esp

mov eax,[ebp+8]
add eax,[ebp+12]

pop ebp
ret 8
addxy endp

main proc
_start::
push ebp
mov ebp,esp

push y ;先压y
push x ;再压x

call addxy

xor eax,eax
pop ebp
ret
main endp
end _start

cdecl

参数从右至左压入堆栈,调用者负责平衡堆栈

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
.code
addxy proc
push ebp
mov ebp,esp

mov eax,[ebp+8]
add eax,[ebp+12]

pop ebp
ret
addxy endp

main proc
_start::
push ebp
mov ebp,esp

push y ;先压y
push x ;再压x

call addxy

add esp,8 ;平衡堆栈

xor eax,eax
pop ebp
ret
main endp
end _start

fastcall

前两个参数用 ECX/EDX,子程序(被调函数)负责平衡堆栈

例题

eg:编写汇编语言子程序,实现C表达式SUM=X+Y的功能

  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
.data
x1 dd 5
y1 dd 6
.code
addxy proc
push ebp
mov ebp,esp

add eax,ebx

pop ebp
ret
addxy endp

main proc
_start::
push ebp
mov ebp,esp

mov eax,x1
mov ebx,y1

call addxy

xor eax,eax
pop ebp
ret
main endp
end _start
  1. 函数的参数传递采用堆栈实现,要求函数的形式为int addxy(int ,int)[传值调用]

C语言函数声明

1
int addxy(int x,int y);

汇编实现

压栈顺序为 y -> x

堆栈示意图

image-20250605141425030

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
.data
x1 dd 5
y1 dd 6
.code
addxy proc
push ebp
mov ebp,esp

mov eax,[ebp+8]
add eax,[ebp+12]

pop ebp
ret 8
addxy endp

main proc
_start::
push ebp
mov ebp,esp

push y1
push x1

call addxy

xor eax,eax
pop ebp
ret
main endp
end _start
  1. 函数的参数传递采用堆栈实现,要求函数的形式为void addxy(int ,int,int*)[传址调用]

C语言函数声明

1
void addxy(int x,int y,int* sum);

汇编实现

压栈顺序: int* sum -> y -> x

堆栈示意图:

image-20250605141634503

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
.data
x1 dd 5
y1 dd 6
sum dd 0
.code
addxy proc
push ebp
mov ebp,esp

mov eax,[ebp+8]
add eax,[ebp+12]

mov [esi],eax ;*sum = x+y

pop ebp
ret 12
addxy endp

main proc
_start::
push ebp
mov ebp,esp

lea esi,sum

push esi
push y1
push x1
call addxy

xor eax,eax
pop ebp
ret
main endp
end _start
  1. 结构体传参

在汇编中定义结构体

1
2
3
4
5
6
7
8
结构体名 struct
x1 dd ?
y1 dd ?
sum dd ?
结构体名 ends

.data
变量名 结构体名 <1,2,0> ;{x1=1,y1=2,sum=0}

传参时,应该是传址调用,因此我们需要传入结构体变量的地址

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
dataseg struct
x1 dd ?
y1 dd ?
sum dd ?
dataseg ends

.data
data dataseg <5,6,0>
.code
addxy proc
push ebp
mov ebp,esp

mov eax,[esi] ;(&data)->x传送到eax
add eax,[esi+4] ;(&data)->y加到eax

mov [esi+8],eax ;eax中的值传送到(&data)->sum

pop ebp
ret 4 ;平衡堆栈
addxy endp

main proc
_start::
push ebp
mov ebp,esp

lea esi,data ;&data

push esi
call addxy

xor eax,eax
pop ebp
ret
main endp
end _start

5)改正下面程序的问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
;调用程序

Mov bx,10
Mov cx,20
Call fun
Add bx,cx
Add ax,bx

;被调用程序
Fun proc
Xor ax,ax
Mov cx,5
Mov bx,1
Shl bx,cx
Ret
Fun endp

1.bx,cx寄存器在调用程序段和被调用程序段都被使用,却没有压进堆栈中进行保护,导致子程序段中破坏了它们

2.左移位指令shl使用错误,移位次数应该放在cl中而不是cx

3.子程序的结果并没有存到ax中,导致子程序的结果丢失,ax中的值始终是0

思考

1.标准C函数参数结合顺序从右至左是什么意思

​ 答:参数压入堆栈的顺序从右至左

2.栈在程序中的作用

​ 答:保护寄存器,存储临时变量,传递参数

3.从机器执行的角度理解标准C中传值和传地址是什么意思

​ 答: 传值:压入堆栈的是原变量的副本,在堆栈中不会影响原变量的值,影响的只是原变量的副本

​ 传址:压入堆栈的是原变量的地址,在堆栈中可以改变地址指向的值从而影响原变量

4.系统调用是指什么,怎么实现的

​ 答:程序调用操作系统提供的函数,通过引用操作系统提供的动态库(.lib)

5.为什么函数只能返回一个值

​ 答:函数返回值通过eax寄存器,但是eax寄存器只有一个

6.函数调用时如何转到被调用的函数,又是如何返回的

​ 答:call指令相当于两条指令:

​ 1.将call指令的下一条指令地址压入堆栈作为返回地址

​ 2.jmp 子程序

7.函数的入口地址是什么概念,为什么C语言可以通过指向函数的指针调用函数

​ 答:函数首条指令的地址;函数指针中存放的是函数首条指令的地址,而汇编中调用函数就相当于 call [函数首地址]

8.编写子程序时为什么要注意寄存器的保护

​ 答:在子程序中使用寄存器,要先压入堆栈进行保护,使用完毕后再弹出,这是因为,子程序中直接使用寄存器会破坏其中的值,特别是这个寄存器如果在调 用者中还要再次使用,如果值被破坏,那么程序最后的执行结果就会出错

9.Call指令和RET的指令的作用是什么,RET后面跟的常数是什么意思,为什么要在后面跟常数?

​ 答:

​ call指令:调用子程序同时将调用程序的下一条指令地址压入堆栈

​ ret指令:程序段执行完毕,返回操作系统

​ 常数:告诉操作系统平衡堆栈需要几个字节

​ 为什么:需要平衡堆栈,避免堆栈不平衡导致栈溢出

系统调用

在vs2017中,因为我们创建的项目是C++空项目,所以项目属性中已经帮我们引用了对应的系统调用的库

image-20250605151434968

声明函数

声明或者调用的时候应当注意参数通常声明为DWORD的数据就可以了,但是要注意,如果在C|C++中声明的是指针,调用的时候需要加上ADDR/offset

以ReadFile为例

1
2
3
4
5
6
7
BOOL ReadFile(
HANDLE hFile, // 文件/设备的句柄
LPVOID lpBuffer, // 接收数据的缓冲区指针
DWORD nNumberOfBytesToRead, // 要读取的字节数
LPDWORD lpNumberOfBytesRead, // 实际读取的字节数(输出参数)
LPOVERLAPPED lpOverlapped // 用于异步操作的 OVERLAPPED 结构指针(可选)
);

在汇编中声明

1
2
3
4
5
6
ReadFile proto  
hFile:dword,
lpBuf:dword,
notr:dword,
lobr:dword,
lol:dword

为什么使用proto呢?

使用proto声明就可以忽略函数的修饰名

如果使用extrn

1
EXTRN	__imp__ReadFile@20:PROC

函数名太繁琐

调用函数

使用invoke(更方便)

1
invoke WriteFile,_hout$[ebp],offset msg,msg_len,addr _cWritten$[ebp],00h

使用call

1
2
3
4
5
6
7
8
9
push 00h                    ; lpOverlapped (最后一个参数先压栈)
lea eax, _cWritten$[ebp] ; lpNumberOfBytesWritten 的地址
push eax
movsx eax,msg_len ; nNumberOfBytesToWrite(若msg_len是变量,需用 offset)
push eax
lea eax, msg ; lpBuffer
push eax
push _hout$[ebp] ; hFile
call WriteFile

使用call更加麻烦,需要自己将参数压入堆栈(但是不用手动平衡堆栈)

例题

将下面的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
46
47
48
#include "stdafx.h"
#include <windows.h>
HANDLE hStdout, hStdin;
int main(void)
{
LPSTR lpszPrompt1 = "Type a line and press Enter, or q to quit: ";
CHAR chBuffer[256];
DWORD cRead, cWritten;
// Get handles to STDIN and STDOUT.
hStdin = GetStdHandle(STD_INPUT_HANDLE);
hStdout = GetStdHandle(STD_OUTPUT_HANDLE);
if (hStdin == INVALID_HANDLE_VALUE ||
hStdout == INVALID_HANDLE_VALUE)
{
return 1;
}

// Write to STDOUT and read from STDIN by using the default
// modes. Input is echoed automatically, and ReadFile
// does not return until a carriage return is typed.
//
// The default input modes are line, processed, and echo.
// The default output modes are processed and wrap at EOL.

while (1)
{
if (!WriteFile(
hStdout, // output handle
lpszPrompt1, // prompt string
lstrlenA(lpszPrompt1), // string length
&cWritten, // bytes written
NULL)) // not overlapped
{

return 1;
}

if (!ReadFile(
hStdin, // input handle
chBuffer, // buffer to read into
255, // size of buffer
&cRead, // actual bytes read
NULL)) // not overlapped
break;
if (chBuffer[0] == 'q') break;
}
return 0;
}
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
.386

.model flat,stdcall

option casemap:none

GetStdHandle proto nStdHandle:dword

ReadFile proto hFile:dword,
lpBuf:dword,
notr:dword,
lobr:dword,
lol:dword

WriteFile proto hFile:dword,
lpBuf:dword,
notr:dword,
lobr:dword,
lol:dword

STD_INPUT_HANDLE = -10
STD_OUTPUT_HANDLE = -11
INVALID_HANDLE_VALUE = 0FFFFFFFFH
BUF_LEN = 255

.data
msg db "Type a line and press Enter, or q to quit: ",00dh,00ah ;00dh,00ah 为\r\n
msg_len db $-msg
chBuffer db BUF_LEN dup(0)
.code

main proc
_start::
_hout$ = -16
_hin$ = -12
_cRead$ = -8
_cWritten$ = -4

push ebp
mov ebp,esp

sub esp,16 ;给局部变量留空间

invoke GetStdHandle,STD_INPUT_HANDLE
mov _hin$[ebp],eax
invoke GetStdHandle,STD_OUTPUT_HANDLE
mov _hout$[ebp],eax

mov ebx,_hin$[ebp]
cmp ebx,INVALID_HANDLE_VALUE
mov ebx,_hout$[ebp]
je InvalidError
cmp ebx,INVALID_HANDLE_VALUE
je InvalidError
infloop:
invoke WriteFile,_hout$[ebp],offset msg,msg_len,addr _cWritten$[ebp],00h
cmp eax,0
je InvalidError

invoke ReadFile,_hin$[ebp],offset chBuffer,255,addr _cRead$[ebp],00h
cmp eax,0
je NormalEnd

lea edi,chBuffer
mov al,[edi]
cmp al,071h
je NormalEnd

jmp infloop

NormalEnd:
add esp,16
xor eax,eax
pop ebp
ret

InvalidError:
add esp,16
mov eax,1
pop ebp
ret
main endp
end _start

思考:

系统调用/API是什么,程序员为什么通常要了解特定系统的系统调用/API?

​ 系统调用:是内核提供的底层接口,通过软中断(如 int 0x80)或专用指令(如 syscall)触发

​ API:应用程序接口,是高级语言对系统调用的封装,可能涉及多个系统调用。

​ 不同的操作系统有不同的系统调用,不了解他们会使自己写的程序不具有跨平台移植性

​ 同时,系统调用是别人造好的轮子;用别人造好的轮子,写程序更加方便高效

什么是HANDLE?有什么用?

句柄,用来标识对象的标识符,用来描述窗口,文件等

句柄隐藏了资源的具体实现细节,应用程序只需通过句柄与资源进行交互,而无需关心资源的存储位置和内部结构,这使得资源管理更加简单和高效

模块化编程

要点

与高级语言类似,将不同的功能拆分到不同的.asm文件中

汇编的主程序入口是end后的标号,所以在子程序模块中的.code段中,end后就不能再跟上标号或者段名

子程序中需要声明子程序段为public,否则其他程序段可能就无法调用该子程序

调用者中使用proto引用子程序,引用格式与声明格式保持一致(proc -> proto)

以addxy为例

1
int addxy(int x,int y);

addxy.asm声明方法1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
.386

.model flat,stdcall

option casemap:none

.data

public addxy ;这样才能在其他文件中被调用
.code
addxy proc stdcall x:dword,y:dword
push ebp
mov ebp,esp

mov eax,x
add eax,y

xor eax,eax
pop ebp
ret 8 ;stdcall规定由被调用者平衡堆栈
addxy endp
end

addxy.asm声明方法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
.386

.model flat,stdcall

option casemap:none

.data

public addxy
.code
addxy proc
_x$ = 8
_y$ = 12

push ebp
mov ebp,esp

mov eax,_x$[ebp]
add eax,_y$[ebp]

pop ebp
ret 8 ;stdcall规定由被调用者平衡堆栈
addxy endp
end

main.asm

对于第一种声明方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
.386

.model flat,stdcall

option casemap:none

addxy proto stdcall x:dword,y:dword ;引用与声明格式一致,最好使用proto引用

.data

.code
main proc
_start::
push ebp
mov ebp,esp

invoke addxy,4,5

xor eax,eax
pop ebp
ret
main endp
end _start

对于第二种声明方式

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
.386

.model flat,stdcall

option casemap:none

addxy proto ;或extrn addxy:proto

.data
x dd 5
y dd 6
.code
main proc
_start::
push ebp
mov ebp,esp

push y
push x

call addxy

xor eax,eax
pop ebp
ret
main endp
end _start

例题

编写汇编程序完成以下的C语言代码的功能

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
int addxy(int x,int y)
{
return x+y;
}
int main()
{
int x;
int y;
int sum;
scanf("%d",&x);
scanf("%d",&y);
sum=addxy(x,y);
printf("%d",sum);
return 0;
}

func.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
.386

.model flat,stdcall

option casemap:none

GetStdHandle proto stdcall nStdHandle:dword

public func
.code
func proc stdcall nStdHandle:dword
invoke GetStdHandle,nStdHandle
ret
func endp
end

input.asm

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
.386

.model flat,stdcall

option casemap:none

STD_INPUT_HANDLE = -10
INVALID_HANDLE_VALUE = 0FFFFFFFFH
BUF_LEN = 255

func proto stdcall nStdHandle:dword
ReadFile proto stdcall hFile:dword,lpBuffer:dword,nNumberOfBytesToRead:dword,lpNumberOfBytesToRead:dword,lpOverlapped:dword

.data
buf db BUF_LEN dup(0)
public input
.code
input proc stdcall var_addr:dword
;局部变量在堆栈中偏移量
_lol$ = -20
_lobr$ = -16
_lobt$ = -12
_buf$ = -8
_hfile$ = -4

push ebp
mov ebp,esp

sub esp,20

;获取句柄
invoke func,STD_INPUT_HANDLE
mov _hfile$[ebp],eax

push esi
lea esi,buf
;取buf的地址送入堆栈
mov _buf$[ebp],esi
pop esi

invoke ReadFile,_hfile$[ebp],addr buf,BUF_LEN,addr _lobr$[ebp],00h

mov ecx,_lobr$[ebp]

push esi
lea esi,buf

push ebx
push edx
mov dl,10
xor eax,eax
mov dh,00dh
;将输入的字符串转换为数字
convert:
cmp [esi],dh
je end_convert

xor ebx,ebx
mov bl,[esi]
sub bl,030h
imul dl

add eax,ebx
inc esi
loop convert
end_convert:
pop edx
pop ebx
pop esi
add esp,20
pop ebp
;将转换结果再传给参数指向的值
mov ebx,[var_addr]
mov [ebx],eax
ret 4
input endp
end

output.asm

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
.386

.model flat,stdcall

option casemap:none

STD_OUTPUT_HANDLE = -11
INVALID_HANDLE_VALUE = 0FFFFFFFFH
BUF_LEN = 255

func proto stdcall nStdHandle:dword
WriteFile proto stdcall hFile:dword,lpBuffer:dword,nNumberOfBytesToRead:dword,lpNumberOfBytesToRead:dword,lpOverlapped:dword

.data
buf db BUF_LEN dup(0)

public output

.code
output proc stdcall var:dword
_lol$ = -20
_lobw$ = -16
_lobt$ = -12
_buf$ = -8
_hfile$ = -4

push edx
push eax
push ebx
push ecx
push esi

xor edx,edx
xor ecx,ecx
xor ebx,ebx
mov eax,var
mov ebx,10
lea esi,buf
;将数字重新转换成字符,采用压栈逆序转换
convert:
cmp eax,0
je popBuf
xor edx,edx
div ebx
add edx,030h

push edx
inc ecx
jmp convert

popBuf:
pop edx
mov [esi],dl

inc esi
loop popBuf
writeFile:
mov ecx,00h
mov [esi],ecx
pop esi

pop ecx
pop ebx
pop eax
pop edx

push ebp
mov ebp,esp

sub esp,20

invoke func,STD_OUTPUT_HANDLE
mov _hfile$[ebp],eax

push esi
lea esi,buf
mov _buf$[ebp],esi
pop esi

invoke WriteFile,_hfile$[ebp],addr buf,BUF_LEN,addr _lobw$[ebp],00h
add esp,20
pop ebp
ret 4

output endp
end

addxy.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.386

.model flat,stdcall

option casemap:none

.data

public addxy
.code
addxy proc stdcall x:dword,y:dword,sum:dword ;传址
mov eax,x
add eax,y
mov ecx,sum
mov [ecx],eax
ret 12
addxy endp
end

main.asm

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
.386

.model flat,stdcall

option casemap:none

addxy proto stdcall x:dword,y:dword,sum:dword
input proto stdcall var_addr:dword
output proto stdcall var:dword

.data
.code
main proc
_start::
_x$ = -12
_y$ = -8
_sum$ = -4
push ebp
mov ebp,esp
sub esp,12

invoke input,addr _x$[ebp]
invoke input,addr _y$[ebp]

invoke addxy,_x$[ebp],_y$[ebp],addr _sum$[ebp]

invoke output,_sum$[ebp]

add esp,12
xor eax,eax
pop ebp
ret
main endp
end _start