x86_64架构汇编语言

汇编的概念与常用寄存器

汇编语言(Assembly Language)是一种低级编程语言,直接与计算机硬件交互。它是机器语言的符号化表示,人类可以通过汇编语言编写程序,而计算机则通过汇编语言指令与硬件进行通信。汇编语言具有与特定计算机体系结构(如 x86、ARM 等)密切相关的语法和指令集。

主要特点:

  • 低级语言:汇编语言离机器语言非常近,指令与机器代码几乎一一对应。
  • 面向硬件:汇编语言能精确控制计算机硬件资源,如寄存器、内存、I/O 设备等。
  • 高效:因为它直接控制硬件,运行效率极高。
  • 可移植性差:由于与硬件架构紧密相关,汇编语言在不同硬件平台之间的可移植性差。

汇编语言是通过汇编器(Assembler)转换成机器代码(即二进制代码),然后计算机的处理器直接执行这些机器指令。

常见的计算机架构:

x86架构,ARM架构,MIPS架构等等

计算机为什么会选择使用二进制?

因为电路可以很轻松的用二进制表示:

电路开代表0

电路关代表1

常用的汇编寄存器

在计算机体系结构中,寄存器是处理器中的高效存储单元,汇编语言中的寄存器是存储数据、地址或控制信息的地方。寄存器非常快,比内存的访问速度要快得多。不同的处理器架构(如 x86、ARM)有不同的寄存器集,下面以 x86 架构(32位和64位)为例,介绍常用的寄存器。

寄存器分为 32位,16位,8位通用寄存器:

32位:EAX,ECX,EDX,EBX,ESP,EBP,ESI,EDI

16位:AX,CX,DX,BX,SP,BP,SI,DI

8位:AL,CL,DL,BL,AH,CH,DH,BH

寄存器的结构:EAX-AX-AH-AL的对应关系

为什么8位寄存器只能放2个十六进制的数

也就是bit,一个十六进制的数需要4个二进制数才能表示:如

十六进制 二进制
0 0000
A 1010
F 1111

内存单元:字节,每个字节都有个编号,称为内存地址

内存当中的最小单位是字节

内存是有宽度的,一个编号对应的默认宽度是8位

注意左下角:

可以得出堆栈窗口数据(dd)是从后往头往前排,内存窗口数据(db)是从头往后顺序排列

命令:db 0x0012FFDC

0x0012FFDC这个地址真正指向的数据是:E4

对应右边的堆栈来看:

0x0012FFDC————>B6B37CE4(这是dd 0x0012FFDC是以四个字节为一个单位,但是这个地址真正指向的是E4

0x0012FFD9————>真正指向的是7C,从上图的左侧可以看出(db 0x0012FFDC这是一个字节一个单位)

现在我进行如下操作:

1
mov byte ptr ds:[0x0012FFD8],0xAA

可以发现已经变成了AA

还是这行代码:

1
2
3
mov word PTR DS:[12FFD8],0xBBBB
;word 是存入数据的宽度
;所以切记你影响的不是一个这一个数据,有可能影响的这个数据相关联的后面的一些字节,取决于你的宽度

还是这行代码:

1
2
mov dword ptr ds:[12FFD8],0x12345678
;你看这个我们这个地址它虽然只指向一个字节,但是它却影响了四个字节

总结:对于内存的操作范围是需要指定的


怎么来形容寄存器呢?

就是一个容器,用于存储数据

1. 通用寄存器(General-P urpose Registers)

寄存器 编号(二进制) 编号(十进制)
32位 16位 8位
EAX AX AL 000 0
ECX CX CL 001 1
EDX DX DL 010 2
EBX BX BL 011 3
ESP SP AH 100 4
EBP BP CH 101 5
ESI SI DH 110 6
EDI DI BH 111 7

结构图如下:兼容前辈

代码演示:

1
2
3
4
5
6
7
8
9
mov eax,oxAAAAAAAA
mov ax,0BBBB
mov ah,0CC
mov al,0DD
mov ecx,eax

;mov被称为操作码
;作用,将源操作数复制一份放到目标操作数里面去
;注意上述操作要求目标操作数和源操作数的宽度必须是一样的

mov语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
1. mov r/m8,r8
;r/m8:说明其前面既可以是一个8位的寄存器,也可以是一个8位的内存
;mov ch,al
2. mov r/m16,r16
3. mov r/m32,r32

4. mov r8,r/m8
5. mov r16,r/m16
6. mov r32,r/m32

7. mov r8,imm8
8. mov r16,imm16
9. mov r32,imm32

基本概念
r :通用寄存器
m :代表内存
imm :代表立即数
r8 :代表8位通用寄存器
m8 :代表8位内存
imm8 :代表8位立即数(就是能直接用的数,可以当c语言里的常量来理解)

抽象出来:

MOV 目标操作数,源操作数

作用:拷贝源操作数到目标操作数

  • 源操作数可以是立即数,通用寄存器,段寄存器,或者内存单元
  • 目标操作数可以是通用寄存器,段寄存器或者内存单元
  • 操作数宽度必须一样
  • 源操作数和目标操作数不能同时为内存单元

这些寄存器可以用来存储数据、地址和中间结果。它们可以被程序员任意使用。

  • EAX / RAX(累加寄存器):
    • 在 32 位模式下,EAX 被称为累加寄存器;在 64 位模式下,它被称为 RAX
    • 用于算术运算(如乘法和除法)中的结果存储。
    • 常用于返回函数值。
  • EBX / RBX(基址寄存器):
    • 在 32 位模式下,EBX 被称为基址寄存器;在 64 位模式下,它被称为 RBX
    • 常用于存储数据的基地址,尤其是数组、字符串等结构。
  • ECX / RCX(计数寄存器):
    • 在 32 位模式下,ECX 被称为计数寄存器;在 64 位模式下,它被称为 RCX
    • 经常用于循环计数、字符串操作(如 REP 指令)等。
  • EDX / RDX(数据寄存器):
    • 在 32 位模式下,EDX 被称为数据寄存器;在 64 位模式下,它被称为 RDX
    • 常用于存储除法运算中的余数。
  • ESI / RSI(源索引寄存器):
    • 在 32 位模式下,ESI 被称为源索引寄存器;在 64 位模式下,它被称为 RSI
    • 用于指向源数据,特别是字符串操作中。
  • EDI / RDI(目标索引寄存器):
    • 在 32 位模式下,EDI 被称为目标索引寄存器;在 64 位模式下,它被称为 RDI
    • 用于指向目标数据,特别是字符串操作中。
  • ESP / RSP(栈指针寄存器):
    • 在 32 位模式下,ESP 被称为栈指针;在 64 位模式下,它被称为 RSP
    • 指向当前栈的顶部,栈用于存储局部变量、返回地址等。
  • EBP / RBP(基指针寄存器):
    • 在 32 位模式下,EBP 被称为基指针;在 64 位模式下,它被称为 RBP
    • 用于指向当前栈帧的基地址(局部变量的起始位置),主要用于函数调用和返回。

2. 段寄存器(Segment Registers)

这些寄存器用于分段存储器模型,主要用于 16 位和 32 位模式下的内存管理。在 64 位模式下,段寄存器的作用大大减少。

  • CS(代码段寄存器):指向当前代码段。
  • DS(数据段寄存器):指向当前数据段。
  • SS(栈段寄存器):指向当前栈段。
  • ESFSGS:其他段寄存器,通常用于操作特定的内存区域。

3. 指令指针寄存器(Instruction Pointer Register)

  • EIP / RIP

    (指令指针寄存器):

    • 在 32 位模式下,EIP 用来指示下一条将要执行的指令的内存地址。
    • 在 64 位模式下,RIP 执行类似功能。

4. 标志寄存器(Flag Register)

标志寄存器用于保存处理器状态,如运算结果、溢出、进位等信息。常见的标志包括:

  • ZF(零标志):如果运算结果为零,则置位。
  • SF(符号标志):如果运算结果为负数,则置位。
  • OF(溢出标志):如果有溢出,则置位。
  • CF(进位标志):如果有进位或借位,则置位。
  • PF(奇偶标志):用于检查运算结果中 1 的个数是否为偶数。

5. 控制寄存器(Control Registers)

在 x86 架构中,控制寄存器通常用于系统级控制,如内存管理。

  • CR0CR3CR4 等寄存器用于处理器的模式控制,如分页、保护模式、缓存控制等。

内容:

内存格式:

7至0:是8bit,也就是一个字节上图,对应说明:一个内存编号对应一个字节

[编号]:[0x12345678]这就称作内存编号打上中括号,里面就是 立即数,打括号的目的是区分地址立即数

内存读写:

涉及内存读写的一定要设置宽度:

代表宽度的关键字:

byteworddwordqword

1
2
3
4
5
6
7
mov word ptr DS:[0x12345678],0xFFFF
;word:是读写的宽度。要读多少写多少
;ptr:Point代表后面是一个指针(指针的意思就是里面存的不是普通的值,而是个地址)
;DS:段寄存器 先不用管记住就行
;0x12345678:内存编号,必须是32位,前面0可以省略

;注意:地址编号不要随便写,因为内存是有保护的,并不是所有的内存都可以直接读写(需要特别处理)建议地址编号写成esp的值

注意如下:地址编号不要随便写,因为内存是有保护的,不是所有的内存编号都可以读写的

寄存器和内存的区别:

  • 寄存器位于CPU内部,执行速度快,但是比较贵
  • 内存速度相对较慢,但成本较低,所以可以做的很大
  • 寄存器和内存没有本质区别,都是用于存储数据的容器,都是定宽的。
  • 寄存器常用的有8个:EAX,ECX,,EDX,EBX,ESP,EBP,ESI,EDI
  • 计算机中的几个常用计量单位:BYTE WORD DWORD

BYTE:字节 = 8bit

WORD:字 = 16bit

DWORD:双字 = 32bit

1KB = 1024 BYTE

1MB = 1024KB

1GB = 1024MB

  • 内存的数量特别庞大,无法每个内存单元都起一个名字,所以用编号来代替,我们称计算机CPU是32位或者64位,有很多书上说之所以叫32位计算机是因为寄存器的宽度是32位,是不准确的,因为还有很多寄存器是大于32位的。

之所以为32位计算机,是因为其寻址宽度是最大是32位

  • 寻址宽度:它能寻址的最大范围

计算机内存的每一个字节都会对应一个编号(即内存编号的单位是字节):如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
0x00000000      
0x00000001
0x00000002
......
......
......
0xFFFFFFFF

上述可以表述的内存编号一共有 FFFFFFFF + 1 个内存编号
也就是 1 0000 0000(16进制)
转换为4,294,967,296(10进制)
这说明其最多可以表示这么多个字节4,294,967,296Byte
也就是4,194,304KB
也就是4096 MB
也就是4GB

所以32位计算机里面能查找的内存编号最多就是4GB

32位计算机的编号最大是32位,也就是32个1 换成16进制为 FFFFFFFF,也就是说,32位计算机内存寻址的最大范围是4GB

内存的单位是字节,那内存中能存储的信息最多为:FFFFFFFF+1 字节,即4GB。

x86汇编框架

右键项目——>生成依赖项——>生成自定义

选择masm:点击确定

然后新建一个源文件:Entry.asm

然后右键属性:常规——>将平台工具集从2017改为2015

因为有一些库的支持只支持到2015,不支持到2017


它和c/c++是一样的:

首先我们手动调整一下入口点:

第一个程序:Entry.asm

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
.586
.model flat,stdcall

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

extern printf:proc
.data
szHello db 'HelloWorld',0

.code
main proc
;首先是取地址`lea`
lea eax,szHello
push eax
call printf
add esp,4
main endp
end

如果想要弹个消息框呢:

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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0

;代码段
.code
;主函数
main proc
;首先是取地址`lea`
lea eax,szHello
push eax
call printf
add esp,4
invoke MessageBoxA,0,addr szHello,addr szHello,0
invoke ExitProcess,0
main endp
end

效果如下:

数据类型和整数运算

Entry.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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0

;代码段
.code
;主函数
main proc

invoke ExitProcess,0
main endp
end
1
2
3
4
5
;数据类型
BYTE db char ;1字节 //data数据得意思 byte字节
WORD DW short ;2字节
dword dd int ;4字节
qword dq long long ;8字节
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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0
dwIndex dd 12138

;代码段
.code
;主函数
main proc
;数据传送指令
mov eax,1
;相当于int eax = 1

;地址传送指令
lea eax,szHello
;相当于下面的这种操作
;char * szBuffer;
;szBuffer = szHello;

;再举个例子:
lea eax,dwIndex
;这个就相当于
;int * p
;p = &dwIndex
invoke ExitProcess,0
main endp
end

mov eax,1的意思就是把eax里面的值用1来替换,也就是赋值我们可以来看一下寄存器:

下个断点以后,运行程序:

然后把这个CPU段标志段调出来:

接下来F10我们来看他的EAX:

已经被赋值为EAX = 00000001

注意:汇编需要一次一生成,修改完代码之后需要右键项目点击重新生成

lea eax,szHello的将szHello的地址传送给eax,也就是赋值可以来看一下寄存器:

可以看到eax内存储的正是szHello的地址.

下面是对int*型的查看:

6a 2f小端序就应该是 2f 6a 也就是12138


基本运算:

加(自增) 减(自减):

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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0
dwIndex dd 12138

;代码段
.code
;主函数
main proc
mov eax,2
;add 2+3,结果存储到eax中
add eax,3
;自增指令 inc
;++
inc eax
;sub 6-2,结果存储到eax中
sub eax,2
;自减指令dec
;--
dec eax
invoke ExitProcess,0
main endp
end

加法运算:2+3

自增:5++

减法运算:6-2

自减:4–

有符号乘除和无符号乘除:

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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0
dwIndex dd 12138

;代码段
.code
;主函数
main proc
;无符号乘法:mul edx:eax
;与加法不一样这个乘数必须放在eax里
mov eax,0FFFFFFFFh
mov ebx,2
;eax * ebx(这里隐含了一个乘数)
mul ebx
;eax * ebx = 00000001 FFFFFFFE
;高位放在edx中,低位放在eax中

;证明其是无符号的整数
mov eax,80000000h
mov ebx,2
mul ebx
;结果为:1 0000 0000说明其是无符号乘法

invoke ExitProcess,0
main endp
end

结果:这是无符号乘法

接下来展示一下有符号乘法

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
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0
dwIndex dd 12138

;代码段
.code
;主函数
main proc
;有符号乘法指令:imul
mov eax,80000000h
mov ebx,2
imul ebx
;结果为:FFFF FFFF 0000 0000 发生了溢出

invoke ExitProcess,0
main endp
end

这里可以自己写代码调试看寄存器就不展示了。

有符号除法和无符号除法

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
;一些预处理
.586
.model flat,stdcall
option casemap:none
;区分大小写

;包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

;外部函数声明
MessageBoxA proto hWndx:DWORD,lpText:DWORD,lpCaption:DWORD,uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;数据段
.data
szHello db 'HelloWorld',0
dwIndex dd 12138

;代码段
.code
;主函数
main proc
;div idiv
;运算后的结果也是高位edx低位eax——>`edx:eax`
mov eax,101
mov edx,0
;运算前先给高位置0
mov ebx,2
div ebx

invoke ExitProcess,0
main endp
end

商在eax里,余数在edx

101是十进制的除2以后会余数1

位运算指令

Entry.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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
; 主函数
main proc
; & 按位与运算指令:and
; 1 and 2
; 01 and 10 ===> 00
mov eax, 1
mov ebx, 2
and eax, ebx
; 运算完以后存储到 eax 中

; | 按位或运算指令:or
; 1 or 2
; 01 or 10 ===> 11 ====> 3
mov eax, 1
mov ebx, 2
or eax, ebx
; 运算完以后存储到 eax 中

; ~ 非运算指令:not
; 1 ===> 0000 0001 ===> FFFF FFFE
mov eax, 1
not eax
; 运算完以后存储到 eax 中

; ^ 异或运算指令:xor
; 不同为 1,相同为 0
mov eax, 1
mov ebx, 2
xor eax, ebx
; 运算完以后存储到 eax 中

; << 左移运算指令:shl
; 左移 3 位也就是 0001 左移三位 ===> 1000 ===> 8
mov eax, 1
mov cl, 3
shl eax, cl
; 运算完以后存储到 eax 中

; >> 右移运算指令:shr
; 右移 3 位也就是 1111 右移三位 ===> 1000 ===> 8
; cl 就是 ecx的低8位
mov eax, 1110
mov cl, 35
shr eax, cl
; 运算完以后存储到 eax 中

invoke ExitProcess, 0
main endp
end

寻址操作

如果我们想要读某块内存或改某块内存,最最关键的是什么?

最关键的事就是找到他:**寻址**

碰到问题的时候一定要学会动手做实验,因为问题永远问不完,只有学会了自己去做实验

寻址公式1:[立即数]

  • 读取内存的值:
1
2
3
4
5
mov eax,dword ptr ds:[0x13FFC4]
;在0x13FFC4地址内读取宽度为dword(也就是四个字节)读到的分别是C4,C5,C6,C7
;把内存中的数据拿出来放到寄存器
;切记mov的两个操作数不能同时为内存单元
mov eax,dword ptr ds:[0x13FFC8]
  • 向内存中写入数据
1
2
3
mov dword ptr ds:[0x13FFC4],eax
;这个是将寄存器中的值写到内存当中对应的地址中去[0x13FFC4]其范围是C4,C5,C6,C7
mov dword ptr ds:[0x13FFC8],ebx
  • 获取内存编号
1
2
3
4
lea eax,dword ptr ds:[0x13FFC4]
;这是直接获取内存地址编号也就是0x0013FFC4将其存入eax寄存器
;lea不读内存,mov读内存
lea eax,dword ptr ds:[esp+8]

mov指令读的是

lea指令读的是内存编号

寻址公式2:[reg] reg代表寄存器 可以是8个通用寄存器中的任意一个

  • 读取内存的值:
1
2
3
4
mov ecx,0x13FFD8
;ecx中存储的是一个内存地址(也就是编号)
mov eax,dword ptr ds:[ecx]
;将ecx中存储的`内存编号`所对应的值读出来到eax
  • 向内存中写入数据:
1
2
3
mov edx,0x13FFD8
mov dword ptr ds:[edx],0x87654321
;将值 `0x87654321`写入edx这个寄存器所存储的`内存编号`中去
  • 获取内存编号:
1
2
lea eax,dword ptr ds:[edx]
;eax中存的是edx中存储的`内存编号`

寻址公式3:[reg+立即数]

  • 读取内存的值:
1
2
mov ecx,0x13FFD8
mov eax,dword ptr ds:[ecx+4]
  • 向内存中写入数据:
1
2
mov edx,0x13FFD8
mov dword ptr ds:[edx+0xC],0x87654321
  • 获取内存编号:
1
lea eax,dword ptr ds:[edx+4]

寻址公式4:[reg+reg*{1,2,4,8}]

  • 读取内存的值:
1
2
3
4
;从上往下依次执行
mov eax,13FFC4
mov ecx,2
mov edx,dword ptr ds:[eax+ecx*4]
  • 向内存中写入数据:
1
2
3
mov eax,13FFC4
mov ecx,2
mov dword ptr ds:[eax+ecx*4],87654321
  • 获取内存编号:
1
lea eax,dword ptr ds:[eax+ecx*4]

寻址公式5:[reg+reg*{1,2,4,8}+立即数]

  • 读取内存的值:
1
2
3
mov eax,13FFC4
mov ecx,2
mov edx,dword ptr ds:[eax+ecx*4+4]
  • 向内存中写入数据
1
2
3
mov eax,13FFC4
mov ecx,2
moc dword ptr ds:[eax+ecx*4+4],876543321
  • 获取内存编号
1
lea eax,dword ptr ds:[eax+ecx*4+2]

保护模式下的寻址方式

Entry.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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
; 主函数
main proc
;立即数寻址
mov eax,1111h

;寄存器寻址
mov eax,1111h
mov ebx,eax

;直接寻址
lea eax,dwIndex

;寄存器间接寻址
lea ebx,dwIndex
mov eax,[ebx]

;寄存器相对寻址
push ebp
mov ebp,esp
mov esi,[ebp + 4]
mov esp,ebp
pop ebp

;基址加变址寻址
;mov eax,[ebx + esi]

;相对基址加变址寻址
;mov eax,[ebx + esi + 4]

;比例因子寻址
;mov eax,[eax + ecx*4]


invoke ExitProcess, 0
main endp
end

EFLAGS标志寄存器

EFLAGS寄存器

状态寄存器:

CF,ZF,SF,PF,OF,AF

控制寄存器:

DF,IF,TF


高位低位去学:

1.OF(11)溢出标志位[Over Flow flag]

状态:OV(1)/NV(0)

如何判断溢出与否?

如果溢出了调试器上显示的就是OV(1),没有溢出就是NV(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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
; 主函数
main proc
;自己异或自己就是置0
xor eax,eax

;al 代表了eax的低8位也就是1字节
;8位也就是0000 0000 他能表示的最大有符号正数也就是0111 1111:127
;所以98+99>127肯定就是会溢出的:98+99 = -59
mov al,98
add al,99

invoke ExitProcess, 0
main endp
end

我们通过观察8位计算其会发生溢出:98+99=-59

我们发现其溢出了:OV = 1

2.DF(10)方向标志位[Direction flag]

用于控制字符串处理指令的操作方向。它的状态会影响一些字符串操作指令,比如 MOVSCMPSSCASLODSSTOS 等。

状态:DN(1)/UP(0)

DF = 1(DN,Down):表示 向低地址方向 进行操作(通常指向更小的地址值)。即字符串处理时,指令将从 高地址到低地址 逐字节/逐字/逐双字等进行操作。

DF = 0(UP,Up):表示 向高地址方向 进行操作。即字符串处理时,指令将从 低地址到高地址 逐字节/逐字/逐双字等进行操作。

如何设置和清除方向标志位?

**CLD**(Clear Direction Flag):将方向标志 DF 清除,设置 DF = 0(即 向高地址方向操作)。

**STD**(Set Direction Flag):将方向标志 DF 设置为 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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
; 主函数
main proc
mov eax,0
;将方向标志 DF 设置 DF = 1
std
;将方向标志 DF 清除,设置 DF = 0
cld

invoke ExitProcess, 0
main endp
end

3.IF(9)中断标志位[Interrupt flag]

状态:EI(1)/DI(0)

EI(1):表示 CPU 可以响应外部中断。即,当外部设备(如键盘、鼠标、定时器等)发生中断请求时,CPU 会暂停当前执行的指令,转而处理外部中断。这个状态允许外部中断的发生。

DI(0):表示 CPU 不响应外部中断。即,尽管外部设备可能会发出中断请求,但 CPU 会忽略这些请求,并继续执行当前的程序。这个状态禁止外部中断。

指令:

STI:设置中断标志位为 1,允许响应外部中断。
示例:STI 启用外部中断。
CLI:清除中断标志位为 0,禁止响应外部中断。
示例:CLI 禁止外部中断。

4.TF(8)陷阱标记位[Trap flag]

用于控制 单步调试程序调试。它的状态决定了是否在每执行一条指令后触发一个中断,从而允许调试器在每条指令执行时进行检查或操作。

陷阱标记位(TF)状态:

  • TF = 1(Trap Enable):表示 启用单步执行。在此状态下,CPU 在每执行一条指令后会触发一个 单步中断(即 INT 1),这通常用于调试目的。调试器能够捕获这个中断,检查指令执行情况,并控制程序的执行。
  • TF = 0(Trap Disable):表示 禁用单步执行。在此状态下,CPU 不会在每条指令执行后触发中断,程序会继续正常执行,不会被中断打断。

如何设置和清除陷阱标志位?

  • TST(Trap Set) 或 **STI**:设置陷阱标志位为 1,启用单步执行,程序每执行一条指令后都会触发中断 1。
  • CLD(Clear Trap Flag) 或 **CLI**:清除陷阱标志位为 0,禁用单步执行,中断不会在每条指令后触发。

5.SF(7)符号标记位[Sign flag]

记录你指令执行之后是正数还是负数,如果是正数结果就为1,负数结果就为0。它是通过检查运算结果的最高位(符号位)来设定的,常用于处理带符号数的计算。

状态:NG(1)/PL(0)

符号标记位 (SF) 状态:

  • SF = 1:表示 结果为负数。即在算术运算(如加法、减法等)后,运算结果的符号位为 1,表示结果是负数。
  • SF = 0:表示 结果为非负数(正数或零)。即在算术运算后,运算结果的符号位为 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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
; 主函数
main proc

mov eax,0
dec eax
add eax,2

invoke ExitProcess, 0
main endp
end

最后加个2又变为正数故PL位又变更为0了

6.ZF(6)零标记位[Zero flag]

就是看指令运算后结果是不是为 0

状态:ZR(1)/NZ(0)

ZR(1):是0

NZ(0):不是0

可调试如下代码

1
2
3
4
5
6
main proc
mov eax,0
inc eax
dec eax
invoke ExitProcess, 0
main endp

调试结果如下:

7.AF(4)辅助标志位[Auxiliary carry flag]

看加减法到一半时是否形成借位或是进位,有的话状态为1,没有则为0

用于指示 在执行某些算术操作时,低 4 位的进位情况。它主要用于 BCD(Binary-Coded Decimal)算术运算,尤其是在处理二进制编码的十进制数时。

状态:AC(1)/NA(0)

示例代码:

1
2
3
4
5
main proc
mov al,0ffh
add al,1
invoke ExitProcess, 0
main endp

首先我们看没有进位时其为0:

这时+1就会产生进位:此时其标志为1

8.PF(2)奇偶标志位[Parity flag]

看结果中 低8bit的位数中1的个数是奇数还是偶数

最低有效字节:低8位

比方说看0x804:

eax = 00000804

只看04中所含的1的个数,来决定PF的值

只看这里的1的个数:0100也就是奇数所以PF = 0

奇数个1就是 0

偶数个1就是 1

状态:PE(1)/PO(0)

示例代码:

1
2
3
4
5
6
main proc
mov al,1
add al,10
add al,1
invoke ExitProcess, 0
main endp

经过一次加法后变成了11也就是1011,也就是奇数个1所以是0:状态为0

再进行一次加1,也就是偶数个1:状态为1

9.CF(0)进位标志位[Carry flag]

它用于指示 算术运算中是否发生了进位(减法就是借位),或者在一些操作中,是否需要考虑进位的影响。进位标志位通常用于 加法、减法 等算术运算,特别是在处理 无符号数 时。

状态:CY(1)/NC(0)

示例代码:

1
2
3
4
5
6
main proc
mov al,98h
add al,al
add al,al
invoke ExitProcess, 0
main endp

我们进行一次加法运算,其进位了,所以状态为1:

再进行一次加法运算,其并没有发生进位操作,所以状态为0:


在此讲述两个ezflags相关的指令:

pushf 保存eflags寄存器到堆栈

popf将堆栈中的eflags寄存器中的值弹出到eflags寄存器上

JCC指令

TEST指令:&

作用将两个操作数进行一个逻辑与的运算,根据运算结果设置标志位,但是两个数的不会改变,他只会记录两个数 结果的标记位,而不会运算结果,也就是说是有结果的,但是在设置完标记位以后就会被扔掉。

它运算完之后,就是为了记录标记位

常见用法:用这个指令,可以确定某寄存器是否等于0

1
2
3
test eax,eax
;不为0就是0
;为0就是1

示例代码:

1
2
3
4
5
6
7
8
9
10
main proc
mov eax,1001b
test eax,1001b
;这里你会测试它eax与1001b相与的结果
;这里ZR=0
mov eax,0
test eax,1001b
;这时zR=1,但是上面的eax没有任何变化
invoke ExitProcess, 0
main endp

结果为0记录就为1(ZR 0标记位)

反之为0

CMP指令:

作用:该指令是比较两个操作数,实际上,它相当于SUB指令,但是相减的结构并不保存到第一个操作数中。只是根据相减的结果来改变0标志位的,当两个操作数相等的时候,0标志位置1(就是sub指令,但不保留结果,直接影响标记为

1
2
3
4
指令格式:
cmp R/M,R/M/IMM
;R:寄存器/M:内存/IMM:立即数

示例:

1
2
3
4
5
6
cmp ax,word ptr ds:[405000]
;寄存器和内存直接作比较,只要宽度一致即可
cmp al,byte ptr ds:[405000]

cmp eax,dword ptr ds:[405000]

比较无符号数:

  • 目的操作数 < 源操作数:ZF=0,CF=1 CMP A-B zf=0说明结果不为0,cf=1说明发生借位,前面的小于后面的
  • 目的操作数 > 源操作数:ZF=0,CF=0 zf=0说明结果不为0,cf=0说明没有发生借位,前面的大
  • 目的操作数 = 源操作数:ZF=1,zf=1说明结果为0两数相等。
  • 目的操作数 != 源操作数:ZF=0,zf=0说明结果为0两数不相等。
  • 目的操作数 <= 源操作数:CF=1 || ZF=1
  • 目的操作数 >= 源操作数:CF=0

比较有符号数:

  • 目的操作数 < 源操作数:SF!=OF
  • 目的操作数 > 源操作数:SF=OF
  • 目的操作数 = 源操作数:ZF=1
  • 目的操作数 != 源操作数:ZF=0
  • 目的操作数 <= 源操作数:ZF=1 || SF!=OF
  • 目的操作数 >= 源操作数:SF=OF || ZF=1

JCC指令:是一系列jxx的跳转指令:其只看标志寄存器,根据标志寄存器进行跳转。

jmp 无条件跳转:

作用:就一句话修改eip的值

1
2
3
4
5
可以这么来理解:
mov eip,寄存器/立即数
简写为:
jmp 寄存器/立即数
;执行完以后直接修改eip

jmp执行的时候只修改了 eip的值

调试代码:

1
2
3
4
5
6
7
main proc
mov eax,1
flag:
inc eax
jmp flag
invoke ExitProcess,0
main endp

打断点观察:第一次F10

第二次F10:就一直在此跳转了

je/jz指令:

判断ZF = 1,说明其在相等的条件下才会发生跳转 。

调试代码:

1
2
3
4
5
6
7
8
9
10
main proc
mov eax,1
mov ebx,1
flag:
cmp eax,ebx
;经过比较已经改变了标志位
je flag
;根据标志位进行跳转
invoke ExitProcess,0
main endp

根据上面的学习也就是说当cmp比较完两者以后,发现相等,就会发生跳转操作

jne/jnz指令:

判断ZF = 0,说明其在不相等的条件下才会发生跳转

js指令:

判断 SF=1,结果为负数的时候发生跳转

jns指令:

判断 SF=0,结果不为负数的时候发生跳转

jp/jpe指令:

判断 PF=1,结果中1的个数是偶数跳转

jnp/jpo指令:

判断 PF=0,结果中1的个数是奇数跳转

jo指令:

判断 OF=1,结果溢出跳转

jno指令:

判断 OF=0,结果不溢出跳转

jb/jnae指令:

CF=1,无符号,小于就跳转 CF=1

jnb/jae指令:

CF=0,无符号,大于就跳转 CF=0

JBE/JNA指令:

cf = 1 or zf = 1 小于等于

jnbe/ja指令:

cf =0 and zf = 0 大于就跳转

jl/jnge指令:

sf != of 有符号,小于就跳转

JNL/JGE指令:

sf = of 有符号 大于等于跳转

JLE/JNG指令:

zf = 1 or sf != of 小于等于

JNLE/JG指令:

ZF = 0 and sf = of 大于

整理一个查询表:

指令 说明 标志位
JE, JZ 结果为零则跳转(相等时跳转) ZF=1
JNE, JNZ 结果不为零则跳转(不相等时跳转) ZF=0
JS 结果为负则跳转 SF=1
JNS 结果为非负则跳转 SF=0
JP, JPE 结果中1的个数为偶数则跳转 PF=1
JNP, JPO 结果中1的个数为偶数则跳转 PF=0
JO 结果溢出了则跳转 OF=1
JNO 结果没有溢出则跳转 OF=0
JB, JNAE 小于则跳转 (无符号数) CF=1
JNB, JAE 大于等于则跳转 (无符号数) CF=0
JBE, JNA 小于等于则跳转 (无符号数) CF=1 or ZF=1
JNBE, JA 大于则跳转(无符号数) CF=0 and ZF=0
JL, JNGE 小于则跳转 (有符号数) SF≠ OF
JNL, JGE 大于等于则跳转 (有符号数) SF=OF
JLE, JNG 小于等于则跳转 (有符号数) ZF=1 or SF≠ OF
JNLE, JG 大于则跳转(有符号数) ZF=0 and SF=OF

函数和堆栈

思考:

假设我们需要一块内存,有如下要求:

  • 主要用于临时存储一些数据,如果数量很少就放到寄存器中
  • 能够记录存了多少数据
  • 能够非常快速的找到某个数据

聪明的前辈们进行了如下设计:

用两个寄存器:一个(base)寄存器存地址编号,这个编号恰好就是最底层的地址编号

大家都知道一个地址编号对应的内存是一个字节

什么是堆栈?

如何定位数据呢?

ok了我们自己来构建一个栈:

压入数据:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
mov ebx,12FFE0	;栈底BASE
mov edx,12FFE0 ;栈顶TOP
;开局两个指针准备好一个栈顶一个栈底

;第一种写法
mov dword ptr ds:[edx-4],0xAAAAAAAA
;向内存地址中写入值
sub edx,4
;将栈顶上移

;第二种写法
lea edx,dword ptr ds:[edx-4]
;栈顶上移
mov dword ptr ds:[edx],0xBBBBBBBB
;压值入栈

;第三种写法
sub edx,4
mov dword ptr ds:[edx],0xAAAAAAAA

;第四种写法
mov dword ptr ds:[edx-4],0xDDDDDDDD
lea edx,dword ptr ds:[edx-4]

读取值:

1
2
3
4
;栈底+偏移来取:
mov esi,dword ptr ds:[ebx-8]
;栈顶+偏移来取:
mov esi,dword ptr ds:[edx+4]

出栈:

1
2
3
4
;第一种写法
mov eax,dword ptr ds:[edx]
add edx,4

call指令:

作用:调用函数或者说是 子过程的这么一个指令

1
2
3
4
5
6
7
8
可以这么来理解:
push 返回地址(下一条指令地址)
mov eip,地址A/寄存器
;eip存储的是下一条将要执行的程序的地址

简写为:
call 地址A/寄存器
;执行完以后直接修改eip

这里的知识点和上面的是有关联的,jmpcall指令的区别

1
2
3
4
call:在没有执行之前就将下一行的地址算出来了(下一行指令地址 = 当前指令地址 + 当前指令的长度 )

call 跳转以后还会回去
jmp 跳转以后就不回去了

要搞清楚call是如何过去的?

1.保存下一行的地址到堆栈内(下面有自己的操作示例):

push 返回地址

并且此时会将返回地址存入eip

  • 这就是我们call的时候压在栈里面的内容

2.jmp到函数位置

jmp 函数地址

ret指令:

作用:返回调用位置下一行,通常与call指令成对出现

1
2
3
4
5
6
7
8
9
10
11
可以理解为:

lea esp,[esp+4]
mov eip,[esp-4]
先移动栈顶再弹出

又或者理解为:
pop eip

简写为:
ret

1.弹出堆栈内的返回地址到 eip

eip就是你下一行要执行的位置

那么存入堆栈和从堆栈中弹出是如何做的呢?

依靠的是push指令和pop指令

堆栈结构:初始状态下ESP和EBP是一样的:

我们进行一个压栈操作:ESP就会指向其

我们来调试如下汇编代码来感受一下这个过程:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
;这是我自定义的一个加法函数
myadd proc
mov eax,1
mov ebx,2
add eax,ebx
ret

myadd endp

; 主函数
main proc
;这里调用了这个函数,看看call是如何进入的:
call myadd
invoke ExitProcess, 0
main endp
end

我们来打个断点运行查看一下:

此时我F11跟进函数内:这个时候exp的地址已经发生了变化:(这里因为我中间的程序意外终端了所以重新运行了一下所以地址也就变化了)

这时候我们再重新输入esp跟进查看:

内容如下:

那么这个00 07 17 62这个东西是干什么的呢?(这是一个小端序所以反过来写)

这就是我们call的时候压在栈里面的内容

我们可以看看汇编的反汇编!(和调出内存和寄存器的方式一样可以看我前面的笔记)

它进行了一手 push 0

也可以在上方输入地址直接查看对应的地址操作:效果和上面一样的

观察上图会发现它其实就是call的下一行:


通过上面的实验我们可以知道我们是怎么过去的怎么回来的了:那我们是否可以往里面pushpop一些东西呢?

答案是肯定的,这就是我们一个传参的方式:举出如下代码:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code
;这是我自定义的一个加法函数
myadd proc
;要想运用传递过来的参数,我们首先需要push ebp 把原始的ebp保存下来
push ebp
;这里的意思就是在我们的函数内部我们使用ebp寻址,把esp保存过来
mov ebp,esp

;在此我们mov eax,[ebp + 8]看是否和下面的示例图对应上
mov eax,[ebp + 8]
mov ebx,[ebp + 12]
add eax,ebx

;在离开函数之前
;完成之后我们直接将ebp弹出回来
mov esp,ebp
pop ebp

ret

myadd endp

; 主函数
main proc
push 1
push 2
;其可以作为参数传递到函数内部

call myadd

;前面push了两个需要进行平栈操作
add esp,8
invoke ExitProcess, 0
main endp
end

示例图如下:

首先push 1:查看esp

F10再走一步push 2:然后再查看esp

注:esp的值是不断变化的所以我们需要一直反复查看

F11跟进函数myadd:继续查看esp

我查看这个地址:这是我们平栈的操作地址:

F10继续:查看esp

压入ebp的内容,进行一个保存

继续F10下一步:查看ebp

ebp这个位置就是老的ebp地址给保存进去了,和上面的示例图所展示的是一样的

根据上面图展示的48 fa b4 00就是老的ebp地址,68 17 a4 00这里是返回地址,02 00 00 00这是最开始压入栈的内容”push 1和push 2”01 00 00 00

根据接下来的对eax赋值操作来观察esp,[ebp + 8],[ebp +12]

[ebp + 8]:确实就是2

[ebp + 12]:确实就是1

全部恢复:

这时我们再看esp

00 a4 17 68:现在esp上这个位置是什么呢正是我们的返回地址:

F10继续推进:此处正是我们的返回地址

进行平栈操作:继续下一步:

add esp,8:平完之后就结束了

上述就是我们的一个函数调用的流程,以及如何使用堆栈来对其进行一个参数的传递的操作。

常见的一些传参约定:

_cdecl:上述的就是

_stdcall

fastcall

thiscall

数组与串指令

数组的内存读写

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
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

; 代码段
.code

; 主函数
main proc

;初始化索引为0
xor edx,edx

;介绍几种取出数组地址的方法:
;offset和addr 是伪指令也都是取地址的
lea eax,nFF
mov ebx,offset nNumber
;mov ecx,addr nNumber

;数组要如何寻址:addr + index * typesize
;eax + edx*4
mov ecx,20
;设置一个计数器ecx:赋值20次
flag:
mov [eax + edx * 4],ecx
inc edx
loop flag

invoke ExitProcess, 0
main endp
end

运行如下:eaxebx分别存储了nFF数组nNumber数组的首地址

F10下一步,观察地址内存储的值变化:

存入:

继续F10:


相关串指令:

stos 系列:

stosb:1字节

stosw:2字节

stosd:4字节

stosq:8字节

他们作用都是一样的只是操作的数据宽度不一样

示例代码:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

target dd 20 dup(0)
sourec dd 20 dup(0FFFFFFFFh)

; 代码段
.code


; 主函数
main proc
;stos stosb:1字节 stosw:2字节 stosd:4字节 stosq:8字节
;他们作用都是一样的只是操作的数据宽度不一样
mov eax,0FFFFFFFFh
mov edi,offset target

stosb


invoke ExitProcess, 0
main endp
end

生效前edi地址内无变化:如下

达到个赋值的操作:

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
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

target dd 20 dup(0)
sourec dd 20 dup(0FFFFFFFFh)

; 代码段
.code
;这是我自定义的一个加法函数
myadd proc
;要想运用传递过来的参数,我们首先需要push ebp 把原始的ebp保存下来
push ebp
;这里的意思就是在我们的函数内部我们使用ebp寻址,把esp保存过来
mov ebp,esp

;在此我们mov eax,[ebp + 8]看是否和下面的示例图对应上
mov eax,[ebp + 8]
mov ebx,[ebp + 12]
add eax,ebx

;在离开函数之前
;完成之后我们直接将ebp弹出回来
mov esp,ebp
pop ebp

ret

myadd endp

; 主函数
main proc
;stos stosb:1字节 stosw:2字节 stosd:4字节 stosq:8字节
;他们作用都是一样的只是操作的数据宽度不一样
mov eax,0FFFFFFFFh
mov edi,offset target

mov ecx,20
;rep会将后面的指令重复ecx次
rep stosd


invoke ExitProcess, 0
main endp
end

执行结果如下:

lods系列:

lodsb:1字节

lodsw:2字节

lodsd:4字节

lodsq:8字节

这个是从这个数组中取出几个字节赋值给eax如下代码:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

target dd 20 dup(0)
sourec dd 20 dup(0FFFFFFFFh)

; 代码段
.code

; 主函数
main proc
;他们作用都是一样的只是操作的数据宽度不一样
mov esi,offset sourec
lodsd
invoke ExitProcess, 0
main endp
end

赋值给了EAX:如下

movs系列:

movsb:1字节

movsw:2字节

movsd:4字节

movsq:8字节

示例代码:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

target dd 20 dup(0)
sourec dd 20 dup(0FFFFFFFFh)

; 代码段
.code

; 主函数
main proc
;他们作用都是一样的只是操作的数据宽度不一样

mov esi,offset sourec
mov edi,offset target
mov ecx,20
rep movsd

invoke ExitProcess, 0
main endp
end

这是从源串四个字节四个字节的赋值过来:

cmps系列:

cmpsb:1字节

cmpsw:2字节

cmpsd:4字节

cmpsq:8字节

比较

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

;我们的数组也是需要声明到数据段内的
nNumber dd 20 dup(?)
;默认初始化为0
nFF dd 20 dup(0FFFFFFFFh)
;指定初始化为指定数据

target dd 20 dup(0)
sourec dd 20 dup(0FFFFFFFFh)

dwTarget dd 1234h
dwSourec dd 1235h

; 代码段
.code


; 主函数
main proc
;他们作用都是一样的只是操作的数据宽度不一样
mov esi,offset dwSourec
mov edi,offset dwTarget
cmpsd

invoke ExitProcess, 0
main endp
end

只改变标记位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
37
38
39
40
41
42
43
44
45
46
47
48
 ; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;宏关键字EQU
;下面是一个无参宏
MAX EQU 256

;下面来实现一个有参宏:
;有点类似函数的声明
Myadd MACRO nNumber
mov eax,256
add eax,nNumber

endm

; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138

; 代码段
.code

; 主函数
main proc
;直接将宏的值赋给eax
mov eax,MAX

;有参宏的利用
Myadd<2>

invoke ExitProcess, 0
main endp
end

结构体:

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
; 一些预处理
.586
.model flat, stdcall
option casemap:none ; 区分大小写

; 包含的链接库
includelib ucrt.lib
includelib legacy_stdio_definitions.lib
includelib kernel32.lib
includelib user32.lib

; 外部函数声明
MessageBoxA proto hWndx:DWORD, lpText:DWORD, lpCaption:DWORD, uType:DWORD
ExitProcess proto uCode:DWORD
extern printf:proc

;无参宏关键字EQU
;下面是一个无参宏
MAX EQU 256

;下面来实现一个有参宏:
;有点类似函数的声明
Myadd MACRO nNumber
mov eax,256
add eax,nNumber

endm


;结构体
Point struct
;声明两个变量x,y
x word ?
;未初始化:?
y word ?
Point ends


; 数据段
.data
szHello db 'HelloWorld', 0
dwIndex dd 12138
;声明一个结构体对象

;结构体名字 类型 未知的初始化<?>
Mypoint Point <?>


; 代码段
.code
myadd proc nNumberA:dword,nNumberB:dword
xor eax,eax
add eax,nNumberA
add eax,nNumberB
ret
myadd endp

; 主函数
main proc
;直接将宏的值赋给eax
mov eax,MAX
;有参宏的利用
Myadd<2>
;结构体的利用
mov Mypoint.x,123
mov Mypoint.y,456

;换一种函数调用:利用函数调用的伪指令
invoke myadd,Mypoint.x,Mypoint.y

invoke ExitProcess, 0
main endp
end

x64汇编框架

直接生成一个x64的简单exe来分析:

汇编代码如下:

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

myadd proc
xor rax,rax
ret

myadd endp

main proc
;64位规定了调用约定(fastcall):(他规定前四个参数要使用寄存器) rcx rdx r8 r9
;需要先开辟个堆栈
sub rsp,28h

mov qword ptr [rsp + 28h],6
mov qword ptr [rsp + 20h],5
;传入参数:
mov r9,4
mov r8,3
mov rdx,2
mov rcx,1

call myadd
add rsp,28h

;临走前再将其平回来
ret


main endp

end

逻辑很简单。

利用x64dbg打开:

我们一开始在ntdll.dll

所以我们要F9继续执行,运行到自己的临空:到当前jmp位置

我们会发现有两个jmp,一个jmp到main,一个jmp到myadd。

F8跳进去:

这里就是我们自己写的汇编代码了。

接下来我们一段一段的执行来感受指令的作用:

F8下一步开辟堆栈sub rsp,28

在此右键——>转到RSP

清晰可见这是rsp的位置。

F8下两步:

我们会发现堆栈对应的内容已经做出了修改。

F8下四步:

四个寄存器内容也正常改变。

F7跟进函数(这里我调试的时候才发现我汇编代码没有调用函数,于是我从新生成了一个exe进行调试):

这时候我们发现原来:

$+20变成了$+28

$+28变成了$+30

那么思考一下:$+8 , $+10, $+18 ,$+20这四个位置是留给谁的呢?

这四位是预留给四个寄存器的值

那么我们现在就给他放回去:做出如下调整:

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

myadd proc
;先把他还回去然后再抬栈,否则的话我们东西会丢

;分别将寄存器的值对应存进去
mov qword ptr [rsp + 20h],r9
mov qword ptr [rsp + 18h],r8
mov qword ptr [rsp + 10h],rdx
mov qword ptr [rsp + 8h],rcx
sub rsp,28h

;清空一个rax作为一个返回值
xor rax,rax

add rax,qword ptr[rsp + 30h]
add rax,qword ptr[rsp + 38h]
add rax,qword ptr[rsp + 40h]
add rax,qword ptr[rsp + 48h]
add rax,qword ptr[rsp + 50h]
add rax,qword ptr[rsp + 58h]

add rsp,28h

ret

myadd endp

main proc
;64位规定了调用约定(fastcall):(他规定前四个参数要使用寄存器) rcx rdx r8 r9
;需要先开辟个堆栈
sub rsp,28h

mov qword ptr [rsp + 28h],6
mov qword ptr [rsp + 20h],5
;传入参数:
mov r9,4
mov r8,3
mov rdx,2
mov rcx,1

call myadd
add rsp,28h

;临走前再将其平回来
ret


main endp

end

没太看懂。出了点小bug这个程序后面再调调

内联汇编和混合编程

32位的内联汇编:

示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
#include <iostream>
#include <Windows.h>

int main(){
//32位想要直接使用汇编,如下操作:
int nNum = 0;
_asm{
xor eax,eax
mov eax,10
add eax,nNum
mov nNum,eax
}

system("pause");
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
#include <iostream>
#include <Windows.h>

//想要调用函数什么的也很简单,如下:
int myadd(int a, int b) {
return a + b;
}

int main() {
//32位想要直接使用汇编,如下操作:
_asm {

push 1
push 2
//这样就能够调用函数了
call myadd
//平栈操作
add esp,8
}

system("pause");
return 0;
}

运行如下:

64位的混合编程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
;首先在本目录下写下:my.asm

.code
myadd proc
sub rsp,28h

xor rax,rax
add rax,rcx
add rax,rdx

add rsp,28h
ret
myadd endp
end
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
//然后在cpp文件中声明:
#include <iostream>
#include <Windows.h>

//64位想要直接使用汇编,如下操作:声明出来即可
extern "C" long long myadd(long long llNumberA,long long llNumberB);


int main() {

//如下:
long long llRes = myadd(1, 2);
std::cout << llRes << std::endl;

system("pause");
return 0;
}

运行结果如下: