前言:
汇编的话,这里不会讲的很详细,可以去看看王爽的《汇编语言》,也可以看看 王爽《汇编语言》笔记,本文主要是自己对基础的复习与巩固。
Ⅱ汇编基础
2.1 CPU架构
CPU在日常来讲的多的就是叫CPU,处理器,CPU全称为中央处理单元(Central Processing Unit),简称为处理器,其作用就是从内存中读取指令,然后解码执行。
而CPU架构指的就是其内部设计和结构,也叫做微架构(Micorarchitecture),由一堆的硬件电路组成,用于实现指令集所规定的操作或运算。
2.1.1指令集架构
指令集架构(Instruction Set Architecture,ISA),包含了一系列的操作码(opcode),以及由特定的CPU执行基本的命令。而由于指令集是一堆二进制数据,不便于阅读和理解,于是就有了汇编语言(Assembly language),用来对指令集进行描述,每条汇编指令都有对应的指令。
复杂指令集计算机(Complex Instruction Set Computer,CISC)典型代表是x86处理器
--->AMD64,在Linux发行版中,x86-64=amd64,x86=i386
精简指令集计算机(Reduced Instruction Set Computer,RISC)典型代表有ARM,MIPS,DEC Alpha。顾名思义,通过减少指令的数量和简化指令的格式来优化和提高CPU的指令执行效率,较高的执行效率和较低的资源消耗,目前的IOS,Android的大多数移动操作系统和嵌入式系统都运行在此类处理器上。
2.1.2 CISC和RISC的对比
1、指令系统
CISC
计算机的指令系统比较丰富,有专用指令来完成特定的功能。因此,处理特殊任务效率较高。
RISC
设计者把主要精力放在那些经常使用的指令上,尽量使它们具有简单高效的特色。对不常用的功能,常通过组合指令来完成。因此,在RISC 机器上实现特殊功能时,效率可能较低。但可以利用流水技术和超标量技术加以改进和弥补。
2、存储器操作
CISC
机器的存储器操作指令多,操作直接。
RISC
对存储器操作有限制,使控制简单化。
3、程序
CISC
汇编语言程序编程相对简单,科学计算及复杂操作的程序社设计相对容易,效率较高。
RISC
汇编语言程序一般需要较大的内存空间,实现特殊功能时程序复杂,不易设计。
4、中断
CISC
机器是在一条指令执行结束后响应中断。
RISC
机器在一条指令执行的适当地方可以响应中断。
5、CPU
CISC
CPU包含有丰富的电路单元,因而功能强、面积大、功耗大。
RISC
CPU包含有较少的单元电路,因而面积小、功耗低。
6、设计周期
CISC
微处理器结构复杂,设计周期长。
RISC
微处理器结构简单,布局紧凑,设计周期短,且易于采用最新技术。
7、用户使用
CISC
微处理器结构复杂,功能强大,实现特殊功能容易。
RISC
微处理器结构简单,指令规整,性能容易把握,易学易用。
8、应用范围
CISC
机器则更适合于通用机。
RISC
由于RISC指令系统的确定与特定的应用领域有关,故RISC 机器更适合于专用机。
9.指令
基于80%的工作由20%的指令完成的原则,RISC设计的指令相对较少(x86处理器有专门的进栈指令push和出栈指令pop
ARM没有这类指令,需要通过 加载(load)和回存(store)以及add等多条指令才能完成)
10.寻址方式(详细的可以去看我之前写的一篇文章:计组原理学习笔记):
x86既能处理寄存器中的数据,也能处理存储器中的数据,寻址方式也多样。通常可以分为立即寻址(eg:mov eax,0)、寄存器寻址(mov eax,ebx)、直接寻址(mov eax,[0x200adb])和寄存器间接寻址(mov eax,[ebx])。
ARM由于采用了load/store架构,处理器的运算指令只在执行过程中只能处理立即数,或者寄存器中的数据,而不能回访内存。因此,存储器和寄存器之间的数据交互,由专门的load和store指令负责。
11.寄存器数量
x86只有8个寄存器(EAX、EBX、ECX、EDX、ESI、EDI、EBP、ESP),x86-64增加到了16个(R8~R15),x86只能完全使用栈,x86-64结合使用栈和部分寄存器来传递参数。
ARM通常包含31个通用寄存器,在函数调用上,可以完全使用寄存器来进行传递参数。
而从逆向工程的角度来看,指令长度不固定会更加难以分析,同一段操作码,从不同的地方开始反汇编,可能会出现不同的结果,即指令错位,而在进行PWN的过程中,我们可以利用指令错位来获取一些有用的 gadget 。
2.2 x86/x64汇编基础
这里如果是讲PWN入门的话,主要讲的还是x86和x64的,因为它是最广为人知的处理器架构,主要包括Inter的IA-32、Inter 64处理器以及AMD的AMD与AMD64处理器【AMDyes】。x86-64处理架构包括了Inter的x86-64架构和AMD的amd64架构,我们可以把它看为x86指令集的64位扩展。
2.2.1 CPU操作模式
x86处理器 主要有三个操作模式:保护模式(Protected Mode)、实地址模式(Real Mode)和系统管理模式(System Manage Mode),此外还有一个保护模式的子模式,称为虚拟8086模式(Virtual 8086 Mode)。
进入64位的x64处理器时代后,产生了一种新的运行模式,叫Long Mode(intel手册里还把它叫做IA-32e Mode),传统的三种模式则被统称为Legacy Mode。 Long Mode又分为2种子模式,分别是64位长模式(64-Bit Mode)和64位兼容模式(Compatibility Mode)。
2.2.2语法风格
汇编语法主要有两大派系:AT&T语法 和 Intel语法。
GAS (GNU Assembler) 编译器默认是基于AT&T语法;MASM、NASM等编译器默认基于Intel语法。需要说明的是,GAS汇编器除了支持AT&T语法之外,自己也定义了一些额外的directives,用于辅助完成汇编操作。
AT&T风格 Intel风格
---------------------------------------------------------------------------------------------
寄存器前加% 寄存器无需另加符号
立即数前加$ 立即数无需另加符号
16进制立即数使用0x前缀 16进制的立即数使用h后缀
源操作数在前,目的操作数在后(从前往后读) 目的操作数在前,源操作数在后(从后往前读)
间接寻址使用小括号() 间接寻址使用中括号[]
间接寻址完整格式:%sreg:disp(%base,index,scale) 间接寻址完整格式:sreg:[basereg + index*scale + disp]
操作位数:指令+l、w、b 指令+ dword ptr、word ptr、byte ptr
2.2.3 寄存器与数据类型
x86_64架构中的寄存器可分为以下几类:
通用寄存器 (General-Purpose registers)
状态和控制寄存器(RFLAGS register)
指令寄存器 (RIP)
XMM寄存器
浮点控制和状态寄存器 (MXCSR)
...
如图所示:
emmm在这里还是大概再讲一遍吧,虽然前面写了,但是讲道理我也忘了挺多的
1、通用寄存器
通用寄存器主要用于完成一些通用的功能,包括算数运算、逻辑运算、比较运算、数据转移、地址计算,还可以临时存放常量、中间结果、指针等内容。
- 数据寄存器
EAX EBX ECX EDX
数据寄存器主要用于保存运算的操作数和运算结果,从而节省读取操作数所需的占用的总线以及访问存储器的时间
AX寄存器:通常称为累加寄存器(Accumulater Register)。累加器可用于乘、除、输入/输出等操作,它们的使用频率很高
BX寄存器:基址寄存器(BaseRegister)。用来作为存储器指针使用
CX寄存器:计数寄存器(CounterRegister)。在循环和字符串操作时,要用它来控制循环次数;在位操作中,当移多位时,要用CL来指明移位的位数
DX寄存器:数据寄存器(DataRegister)。在进行乘、除运算时,它可作为默认的操作数参与运算,也可用于存放I/O的端口地址
在16位CPU中,AX、BX、CX和DX不能作为基址和变址寄存器来存放存储单元的地址,但在32位CPU中,其32位寄存器EAX、EBX、ECX和EDX不仅可传送数据、暂存数据保存算术逻辑运算结果,而且也可作为指针寄存器,所以,这些32位寄存器更具有通用性
- 变址寄存器
ESI EDI SI DI。前两个32为,后两个16位,后两个可以看做是前两个的低16位
SI寄存器:SourceIndex
DI寄存器:DestinationIndex
寄存器ESI、EDI、SI和DI称为变址寄存器(IndexRegister),它们主要用于存放存储单元在段内的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便
变址寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果
它们可作一般的存储器指针使用。在字符串操作指令的执行过程中,对它们有特定的要求,而且还具有特殊的功能
- 指针寄存器
EBP ESP
EBP ESP BP SP。前两个32为,后两个16位,后两个可以看做是前两个的低16位
寄存器EBP、ESP、BP和SP称为指针寄存器(PointerRegister),主要用于存放堆栈内存储单元的偏移量,用它们可实现多种存储器操作数的寻址方式,为以不同的地址形式访问存储单元提供方便。
指针寄存器不可分割成8位寄存器。作为通用寄存器,也可存储算术逻辑运算的操作数和运算结果
它们主要用于访问堆栈内的存储单元,并且规定:BP为基指针(BasePointer)寄存器,用它可直接存取堆栈中的数据;SP为堆栈指针(StackPointer)寄存器,用它只可访问栈顶
- 段寄存器
段寄存器是根据内存分段的管理模式而设置的。内存单元的物理地址由段寄存器的值和一个偏移量组合而成的,这样可用两个较少位数的值组合成一个可访问较大物理空间的内存地址
CS:代码段寄存器(CodeSegmentRegister),其值为代码段的端值
DS:数据段寄存器(DataSegmentRegister),其值为数据段的段值
ES:附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值
SS:堆栈段寄存器(StackSegmentRegister),其值为堆栈段的段值
FS:附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值
GS:附加段寄存器(ExtraSegmentRegister),其值为附加数据段的段值
在16位CPU系统中,它只有4个段寄存器,所以,程序在任何时刻至多有4个正在使用的段可直接访问;在32位微机系统中,它有6个段寄存器,所以,在此环境下开发的程序最多可同时访问6个段
32位CPU有两个不同的工作方式:实方式和保护方式。在每种方式下,段寄存器的作用是不同的。有关规定简单描述如下:
实方式:前4个段寄存器CS、DS、ES和SS与先前CPU中的所对应的段寄存器的含义完全一致,内存单元的逻辑地址仍为“段值:偏移量”的形式。为访问某内存段内的数据,必须使用该段寄存器和存储单元的偏移量。
保护方式:在此方式下,情况要复杂得多,装入段寄存器的不再是段值,而是称为“选择子”(Selector)的某个值。 选择子包含访问控制等属性。
- 指令指针寄存器
指令指针EIP、IP(InstructionPointer)是存放下次将要执行的指令在代码段的偏移量。在具有预取指令功能的系统中,下次要执行的指令通常已被预取到指令队列中,除非发生转移情况。所以,在理解它们的功能时,不考虑存在指令队列的情况
标志寄存器
CF:CarryFlag
PF:ParityFlag
AF:Auxiliary CarryFlag
ZF:ZeroFlag
SF:SignFlag
OF:OverflowFlag
2、RFLAGS寄存器
x86_64中,RFLAGS是一个64位寄存器,其每一个比特位有不同的含义。
最常用的标记位有:
进位标识 CF:无符号数的算数运算产生溢出会导致CF被设置;寄存器移位指令也可能会导致CF被设置;等等
溢出标识 OF:两个有符号数的算数运算可能导致OF被设置
奇偶校验位 PF
符号位 SF:用于标识算数指令和逻辑运算指令运算结果的符号
零标识位 ZF:用于标识算数指令和逻辑运算指令运算结果是不是0
3、指令寄存器
指令寄存器(RIP)包含下一条将要被执行的指令的逻辑地址。
通常情况下,每取出一条指令后,RIP会自增指向下一条指令。在x86_64中RIP的自增也即偏移8字节。
但是RIP并不总是自增,也有例外,例如call 指令和ret指令。call指令会将当前RIP的内容压入栈中,将程序的执行权交给目标函数;ret指令则执行出栈操作,将之前压入栈的8个字节的RIP地址弹出,重新放入RIP。
4、浮点数寄存器: XMM/YMM/ZMM
略
...
x86_64的寻址方式
有效地址的计算
在x86_64架构中,有效地址 (effective address) 的计算公式如下:
EffectiveAddress = BaseReg + IndexReg * ScaleFactor + Disp
其中:
BaseReg是基址寄存器,可以是任意一个通用寄存器
IndexReg是索引寄存器,可以是除了RSP之外的任意一个通用寄存器
ScaleFactor的取值可以是1,2,4,8
Disp是偏移量,可以是长度为8bit,16bit,32bit的有符号整数
关于上述公式中各个部分的取值,有一些隐含的规定:
如果没指明偏移量Disp,则默认是0
最终的有效地址长度始终是64bit
在64位模式下,操作数的大小默认还是32位,且有8个通用寄存器;当给每条汇编指令增加REX(寄存器扩展)的前缀后,操作数变为64位,且增加了8个带有标号的通用寄存器(R8~R15)
此外,
64位与32位有着相同的标志位状态;
64位模式下不能访问通用寄存器的高位字节(如AH,BH,CH及DH)。
整数常量
为了避免汇编器讲数字解释为汇编指令或标识符,需要在以字母开头的十六进制数前加0表示,如0ABCDh。
浮点数常量
浮点数常量也叫实数常量,我们通常是以十进制表示浮点数,以十六进制编码浮点数。浮点数中至少包含一个整数和一个十进制的小数点。
字符串常量
字符串常量是用单引号或双引号括起来的字符序列(含空格符)。汇编语言允许字符串常量的嵌套。
字符串常量在内存中是以整数字节序列保存的,字符串“ABCDEFGH”在gdb中显示的样子如下所示:
gef< x/s 0x4005d4
0x4005d4: "ABCDEFGH"
gef< x/gs 0x4005d4
0x4005d4: 0x4847464544434241
gef< x/8x 0x4005d4
0x4005d4: 0x41 0x42 0x43 0x44 0x45 0x46 0x47 0x48
2.2.4 数据传送与访问
MOV指令是最基本的数据传送指令。
MOV指令的基本格式中,第一个参数为目的操作数,第二个操作为源操作数。如语句MOV EAX,ECX 表示将ECX寄存器的值拷贝到EAX中。MOV指令支持从寄存器到寄存器、从内存到寄存器、从寄存器到内存、从立即数到内存和从立即数到寄存器的数据传送,但不支持从内存到内存的直接传输,要想完成数据的传送,必须使用一个寄存器作为中转。
eg:
MOV EAX,0 ; EAX = 00000000H
MOV AL,78H ; EAX = 00000078H
MOV AX,1234H ; EAX = 00001234H
MOV EAX, 12345678H ; EAX = 12345678H
数据访问指令还有XCHG,该指令允许我们交换两个操作数的值,可以是寄存器到寄存器的转换、内存到寄存器的转换,或者是寄存器到内存的交换。
x86汇编语言使用变量名+偏移量来表示一个直接偏移量操作数,如表示一个数组:
.data
testArray BYTE 99h, 98h, 97h, 96h
.code
MOV al, testArray ; al = 99h
MOV bl, [testArray+1] ; bl = 98h
MOV cl, [testArray+2] ; cl = 97h
如果汇编器未实现数组的边界检查,偏移量超出了数组的实际定义范围,将导致数组越界错误。对于双字数组的汇编代码段,需要使用符合数组元素的偏移量才能正确标识数组元素的位置。
2.2.5 算术运算与逻辑运算
总体组成:CPU一般包括寄存器组、运算器和控制器。顾名思义,寄存器组负责存储,运算器负责进行运算,控制器则控制各个通路,使得处理流程得以进行。
一般简单CPU实现的运算操作有算术运算、逻辑运算和移位。算术运算主要包括加法(add)、减法(dec)、增1(inc)、减1(dec)等,逻辑运算主要包括与(and)、或(or)、非(not)、异或(xor),移位(shift)实现左移右移或者不变。
计算机底层的数据均是以补码表示的。两个机器数相加的补码可以先通过分别对两个机器数求补码,然后再相加得到。在采用补码形式表示时,进行加法运算可以把符号位和数值位一起进行运算(若符号位有进位则直接舍弃),结果为两数之和的补码形式。对于机器数的补码减法可以利用与其相反数的加法实现。
这里又得讲一下补码的概念了,好像我自己之前没有写过相关的知识。但是在大学所学的几门课程中均讲到了相关的概念,无奈这个平时用的少,也忘得差不多了
(其实这些概念性的东西有时候读起来脑子挺大的)
1.原码
原码(true form)是一种计算机中对数字的二进制定点表示方法。原码表示法在数值前面增加了一位符号位(即最高位为符号位):正数该位为0,负数该位为1(0有两种表示:+0和-0),其余位表示数值的大小。
(1)一个正数,他的原码就是它对应的二进制数字。
(2)一个负数,按照绝对值大小转换成的二进制数,然后最高位补1,就是负数的原码
2.反码
反码通常是用来由原码求补码或者由补码求原码的过渡码。
(1)正数的反码与原码相同。
(2)负数的反码是对该数的原码除符号位外各位取反(1变为0,0变为1)
(3)符号位:符号位就是在二进制中用来表示一个数字的正或者负﹐是最高位(最左边的一位),1表示负数,0表示正数。
3.补码
补码定义:
- 最高位为符号位. 为0时为正数. 为1时为负数. 这个与原码相同.
- 为正数时,与原码相同.不作任何处理.
- 为负数时,对原码进行如下处理:
符号位不变, 其它位取反加1.
(1)正数的补码与原码相同。
(2)负数的补码为对该数的原码除符号位外各位取反(就是负数本身的反码),然后在最后一位加1。原码 补码 十进制值 十六进制
0000 0000 0 0x0
0001 0001 1 0x1
0110 0110 6 0x6
1011 1101 -3 0xD
1100 1100 -4 0xC
最简单的算术运算指令是INC和DEC,分别用于操作数加一和操作数减一。这两条指令的操作数既可以是寄存器,也可以是内存。
.data
testWord WORD 1000h
.code
INC EAX
DEC testWord
ADD指令将长度相同的操作数进行相加操作。
.data
testData DWORD 10000h
testData2 DWORD 20000h
.code
MOV EAX, testData ; EAX=10000h
ADD EAX, testData2 ; EAX=30000h
SUB指令为减法操作,将从目的的操作数中减去源操作数。
.data
testData DWORD 20000h
testData2 DWORD 10000h
.code
MOV EAX, testData ; EAX=20000h
MOV EAX, testData2 ; EAX=10000h
在汇编语言中存在标志位寄存器,使用SUB,ADD等指令都可能会造成整数溢出、符号位等标志位发生变化,因此进入标志位、零标志位、溢出标志位、辅助标志位和奇偶标志位都将根据存入的输入发生变化。
NEG指令是把操作数转换为二进制补码,并将操作数的符号位取反。
逻辑运算
与运算
与运算就是要两个二进制数都为1结果才为1,否则都为0的一种运算。
就是将A和B的每位2进制数进行相与。算数表达式为 F=AB。
1010
0101
----
0000
或运算
或运算就是两个二进制数中只要有一个数为1结果就为1,除非两个数都为0的情况下才为0。
就是将A和B的每位2进制数进行相或,算数表达式为F=A+B。
1010
0101
----
1111
非运算
非运算就是将二进制的每一位取反,0变成1,1变成0。此时只有一个数参与运算。
其实意义就是将值取反,算数表达式为F=/A
异或运算
异或运算就是两个二进制数相同为0,相异为1
就是将A和B的每一位进行异或运算。算数表达式为F=A⊕B。
1010
0101
----
1111
简单的提一下,可能哪里有错误,人现在有点麻了(今天状态有点差,如果看到哪里有错误请及时联系我更正,Orz Orz Orz!)
2.2.6跳转指令与循环指令
一般情况下,CPU是顺序加载并执行程序的。但是,指令集中会存在一些条件型指令,将根据CPU的标志位寄存器决定程序控制流的走向。在x86汇编语言中,每一个条件指令都隐含着一个跳转指令。跳转指令有两种类型:有条件跳转和无条件跳转。无条件跳转就是无论标志位寄存器为何值,都会跳转;条件跳转就是当满足某些条件时,程序出现分支,各类分支结构可以组合成不同的程序逻辑。
JMP指令是无条件跳转指令,在编写汇编语言时需要使用一个标识来标识,汇编器在编译时就会将该标号转换为相应的偏移量。一般情况下,该标号必须和JMP指令位于同一函数中,但使用全局标号则不受限制。
JMP labell
MOV EBX, 0
labell:
MOV EAX, 0
JMP指令也可以创建一个循环,也就是在循环结束时用JMP指令再跳回循环开始的位置。由于JMP是无条件跳转,所以除非使用其他方式退出,该循环将一直运算下去。
LOOP指令也可以创建一个循环代码块,ECX寄存器为循环的计数器(实地址模式中略有不同,CX寄存器是LOOP指令与LOOPW指令的默认循环计数器,ECX寄存器为LOOPD指令的循环计数器,64位的x86汇编语言LOOP指令使用RCX为默认循环计数器),每经过一次循环,ECX的值将减一。
MOV AX, 0
MOV ECX, 3
L1:
INC AX
LOOP L1
XOR EAX, EBX
LOOP指令执行分两步,第一步是ECX值减一;第二步将ECX与0比较,如果ECX不为0,则跳转到标号地址处;如果ECX为0,则不发生跳转,执行LOOP的下一条指令。
2.2.7栈与函数的调用
栈是计算机中最重要也是最基础的数据结构之一,它遵循先入后出(后入先出)的规则。
栈空间是计算机内存中的一段确定的内存区域,也有指针指向相应的内存地址,在x86架构中这个指针卫浴ESP寄存器,而在x86-64上为RSP寄存器。
在计算机底层,栈的主要几个用途是:
(1)存储局部变量;
(2)执行CALL指令调用函数时,保存函数地址以便函数结束时正确返回;
(3)传递函数参数。
操作栈的常用指令是PUSH(入栈)和POP(出栈)。PUSH指令会对ESP/RSP/SP寄存器的值进行减法运算,并使其减去4(32位)或8(64位),将操作数写入到上述的寄存器中指针指向的内存中。POP指令是PUSH指令的逆操作,先从ESP/RSP/SP寄存器(即栈指针)指向的内存中读取数据写入其他内存地址或寄存器,再依据系统架构的不同将栈指针的数值增加4(32位)或增加8(64位)。
举个栗子:通过汇编代码实现EAX和EBX值交换,入栈过程:
MOV EAX, 1234h
MOV EBX, 5678h
PUSH EAX
PUSH EBX
POP指令为PUSH指令的反操作,
POP EAX
POP EBX
---使用栈保存函数返回地址
CALL指令调用某个子函数时,下一条指令的地址作为返回地址保存到栈中,等价于PUSH返回地址与JMP函数地址的指令序列。被调用函数结束时,程序将执行ret指令跳转到这个返回地址,将控制权交还给调用函数。等价于POP返回地址与JMP返回地址的指令序列。因此无论调用了多少层子函数,由于栈后入先出的特性,程序控制权最终会回到main函数。
调用子函数这一行为使用PROC与ENDP伪指令来定义,且需要分配一个有效标识符,所有的x86汇编程序中都包含标识符为main的函数,这是程序的入口点,main函数不需要ret指令,但其他的被调用函数结束时都需要通过ret指令将控制权交还调用函数。
... .code
... main PROC
0x00008000 MOV EBX, EAX
... ...
0x00008020 CALL testFunc
0x00008025 MOV EAX, EBX
... ...
... main ENDP
... ...
0x00008A00 testFunc PROC
... MOV EAX, EDX
... ...
... RET
... testFunc ENDP
在执行CALL指令时候,下一条的指令的地址0x00008025被压入栈中,被调用函数testFunc的地址0x00008A00则被加载至EIP寄存器:
High address ... EIP 00008A00
| 00008025 <===ESP
|
|
Low address ...
执行ret指令时,将分为两个过程。第一步,ESP指向的数据将被弹出至EIP寄存器;第二步,ESP的数值增加,将指向栈中的上一个值。
High address ... <===ESP EIP 00008025
| 00008025
|
|
Low address ...
---使用栈传递函数参数
在x86平台中,最常见的参数传递调用的约定是cdecl,其他的还有stdcall、fastcall和thiscall等,我们可以使用栈传递参数,在x86-64上,也可以通过寄存器传递参数。
假设函数func有三个参数arg1,arg2,arg3,那么在cdecl约定下通常如下所示:
push arg3
push arg2
push arg1
call func
此外,被调用函数并不知道调用函数向他传递了多少参数,因此对于参数数量可变的函数来说,就需要说明符标示格式化说明,明确参数信息。常见的printf函数就是参数数量可变的函数之一。
#include <stdio.h>
int main()
{
printf("%d,%d,%d",9998);
return 0;
}
这里输出结果除了9998,还将显示出数据栈内9998之后两个地址的随机数(通常这种数据是被调用函数内部的局部变量)。
---使用栈存储变量
由于MOV指令不允许将标志位寄存器的值复制到一个变量,因此使用PUSHFD指令就是保存标志位寄存器中标志位的最佳途径。PUSHFD指令把32位EFLAGS寄存器的内容压入栈中,POPFD指令则把栈顶部数据弹出至EFLAGS寄存器中。因此当我们需要保存标志位寄存器的值又将其恢复为之前值得时候,可以使用:
PUSHFD
...
POPFD