文章目录
系列目录: Processor Architecture - 处理器体系架构
原文: x86 Architecture
In this article - 此文
Intel x86 处理使用的是复杂指令集的计算机(complex instruction set computer (CISC),还有什么是RISC?,RISC架构)体系架构,意味着使用的是少量的特定寄存器的数量,而不是使用巨量的通用寄存器的方式。也意味着复杂的特定指令将更有优势。
x86 处理器至少可以追溯到 8位的Intel 8080 处理器。x86指令集中的许多特性都是原由于处理器(及其Zilog Z-80变体)的向后兼容性。
Microsoft Win32 的Flat Mode(平坦模式1)使用的是 x86 处理。这个文档将近关注于在flat mode上的。
Register - 寄存器
x86 体系架构由以下非特权的整形寄存器组成。
寄存器 | 描述 |
---|---|
eax | 累加器,运算用(Accumulator) |
ebx | 基地址寄存器(Base Register) |
edx | 数据寄存器 - 用于I/O端口访问及算术函数(Data Register) |
esi | 源索引寄存器(Source index register) |
edi | 目标索引寄存器(Destination index register) |
ebp | 基地址指针寄存器(Base poinster register) |
esp | 栈指针寄存器(Stack pointer) |
所有整形寄存器都是32位的。然而,有许多的16位或是8位的子寄存器。
寄存器 | 描述 |
---|---|
ax | eax的低16位寄存器(Low 16 bits of eax) |
bx | ebx的低16位寄存器(Low 16 bits of ebx) |
cx | ecx的低16位寄存器(Low 16 bits of ecx) |
dx | edx的低16位寄存器(Low 16 bits of edx) |
si | esi的低16位寄存器(Low 16 bits of esi) |
di | edi的低16位寄存器(Low 16 bits of edi) |
bp | ebp的低16位寄存器(Low 16 bits of ebp) |
sp | esp的低16位寄存器(Low 16 bits of esp) |
al | eax的低8位寄存器(Low 8 bits of eax) |
ah | ax的高8位寄存器(High 8 bits of ax) |
bl | ebx的低8位寄存器(Low 8 bits of ebx) |
bh | bx的高8位寄存器(High 8 bits of bx) |
cl | ecx的低8位寄存器(Low 8 bits of ecx) |
ch | cx的高8位寄存器(High 8 bits of cx) |
dl | edx的低8位寄存器(Low 8 bits of edx) |
dh | dx的高8位寄存器(High 8 bits of dx) |
操作子寄存器影响的仅仅只有子寄存器,而不会影响之外的其他子寄存器。例如,储存一些数据到 ax 寄存器,eax 剩余的高16位寄存器是不会有变化的。
当使用 ?(计算表达式) 命令,寄存器的前缀要带上 “at” 标记(@)。例如,你使用使用 ? @ax 而不是 ? ax 。确保调试器能认出 ax 是个寄存器(Register)而不是一个 标记符号(Symobl)。
然而,在r(寄存器)命令下不需要(@)。例如, r ax = 5 将会被正确解析。
还有其他两个表示处理器当前状态的重要寄存器:
寄存器 | 描述 |
---|---|
eip | 指令指针寄存器(instruction pointer) |
flags | 标志寄存器(flags) |
指令指针寄存器(instruction pointer)指向的是即将被执行的指令的地址。
标记寄存器(flags register)是单个bit位(single-bit)的标记集合。许多的指令都会修改 flags 的为,代表指令的结果。这些flags标记可以被后续的条件跳转指令(conditional jump)用于测试用。详情查考 x86 Flags。
Calling Conventions - 调用约定
x86 体系架构有一些不同的调用约定。还好的是,他们都是下面一些寄存器的备份恢复与方法返回规则:
- 函数必须备份恢复所有寄存器,除了 eax,ecx 和 edx,通过
call
可以改变这些寄存器,而 esp,必须通过对应的调用约定来更新。(译者jave.lin:文档真的不用心,call汇编是先push eip + sizeof(eip),就是先把call的以下个指令的地址push到栈中,在跳转到方法签名中的函数指令地址,所以有两个步骤在里头,但这个文档说的不够详细,这微软写x86指令文档这么儿戏的吗?手动哭笑一下。) - eax 寄存器用于函数结果的<=32位的返回值。如果结果是64为的,那么如果储存在
edx:eax
寄存器对中。
下面是 x86 体系架构的调用约定项:
- Win32( __stdcall )
函数的参数是通过从右向左的传入栈中,并且是在被调用函数(callee)中清理栈的。 - C++内置的方法调用(大家了解的:thiscall)
函数参数也是从右向左传入栈,this指针是通过 ecx 寄存器传入的,也是在callee中清理栈的。 - COM(__stdcall C++的方法调用)(译者jave.lin:这不就和上面的Win32一样了么?)
从右向左,this指针会传入,callee清理栈(描述意思和上面thiscall一样,这里简写,不知道为何要多此一举) - __fastcall
头两个DWORD或是更小的字(DWORD-or-smaller)
参数通过 ecx 与 edx 寄存器传入。剩下的参数都会传入栈,也是从右向左,也是callee清理栈。 - __cdecl(译者jave.lin:cdecl就是:c declaration,就是C中声明用的,为何微软不写这些,这些多么容易理解的点)
函数参数也是从右向左传入栈,但清理栈在调用函数(caller)处理。__cdecl调用约定一般用于变长参数数量。(译者jave.lin:如printf(const char *format, …),因为参数在调用函数是知道的,所以可以清理变长的参数栈)
(函数 从左向右入栈 的有:Fortran,Pascal语言,但实际我没去验证过,在此篇博客上看到:系统栈的工作原理)
下面我来列个表格,好理解一写
Calling Convention Type(调用约定类型) | Passing Parameters (参数传递) | Cleaning Stack(栈清理) |
---|---|---|
__stdcall | 从右到左 | 被调用函数 |
__fastcall | 从右到左 | 被调用函数 |
__cdecl | 从左到右 | 调用函数 |
C++内置的对象方法调用还需传递this对象指针
Debugger Display of Registers and Flags - 调试器显示寄存器与标记
这里有个调式器显示寄存器的示例:
eax=00000000 ebx=008b6f00 ecx=01010101 edx=ffffffff esi=00000000 edi=00465000
eip=77f9d022 esp=05cffc48 ebp=05cffc54 iopl=0 nv up ei ng nz na po nc
cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000286
在用户模式(user-mode)下调式,你可以忽略掉 iopl 与最后一整行(cs=001b ss=0023 ds=0023 es=0023 fs=0038 gs=0000 efl=00000286)的调式信息。
x86 Flag - x86 的标记位
在之前的例子中,第二行结尾两个字母的代码都是flags(标记为)(nv up ei ng nz na po nc)。这些都是 单bit位的寄存器,并且各方面都用到。
下面是 x86 中的 flags 的列表:
(下面的列表格式微软中的文档乱七八糟的,排版出了问题,没人发现,没人处理?这里我自己排版一下,x86 Flags 的文档也可以看这个)
Flag Code | Flag Name | Value Flag Status | Status Description |
---|---|---|---|
of | 溢出标记(Overflow Flag) | 0 nv,1 ov | 无溢出(No overflow), 溢出(Overflow) |
df | 方向标记(Direction Flag) | 0 up, 1 dn | 上方(Direction up), 下方(Direction down) |
if | 中断标记(Interrupt Flag) | 0 di, 1 ei | 中断禁用(Interrupts disabled), I中断启用(nterrupts enabled) |
sf | 符号(签名)标记(Sign Flag) | 0 pl, 1 ng | 正数或零(Positive (or zero)), 负数(Negative) |
zf | 零标记(Zero Flag) | 0 nz, 1 zr | 非零(Nonzero), 零(Zero) |
af | 辅助进位标记(Auxiliary Carry Flag) | 0 na, 1 ac | 无辅助进位(No auxiliary carry), 有辅助进位(Auxiliary carry) |
pf | 校验标记(Parity Flag) | 0 pe, 1 po | 偶校验(Parity even), 奇校验(Parity odd) |
cf | 进位标记(Carry Flag) | 0 nc, 1 cy | 无进位(No carry), 有进位(Carry) |
tf | 捕获标记(Trap Flag) | - | 如果 tf 等于1,在执行一个指令后处理器将会抛出 STATUS_SINGLE_STEP 异常。这个标记用于调式器实现 单步调试(single-step)的跟踪。这不应用于其他的应用程序。 |
iopl
I/O特权等级(I/O Privilege Level)
这是一个2 bit 位的整型,值为:0~3之间。用于操作系统控制访问硬件用的。不应该用于应用程序。
当寄存器被显示在调试命令窗口中作为结果,它是以flag status方式显示。然而,如果你想调整flag,可用 r (Registers) 命令,你应该它当做是flag code。
在WinDbg(Windows 调试)的寄存器窗口中,flag code可展示或是修改flags。flag status方式是不支持的。
这里有个例子。之前的寄存器显示中,ng 的flag status有显示。这意味着符号标记的bit为当前为1。要修改的话,使用下面的命令:
r sf = 0
这是将符号标记设置0。如果你要显示其他的寄存器,ng 标记状态码(status code)将不会显示。而是会显示 pl 状态码。
符号标记(Sign Flag),零标记(Zero Flag),和进位标记(Carry Flag)都是最常用的标记。
Conditions - 条件
一个条件描述的是一个或多个标记的状态。在x86 所有操作条件都可有条件术语表示。
汇编器使用一个或多个字母来表示条件。一个条件可以用乘号缩写来表示。例如:AE(“above or equal(高于或等于)”)与NB(“not below(不低于)”)是一样的条件。下面列表中列出一些常见的条件和他们的意思:
Condition Name(条件名称) | Flags(标记) | Meaning(意思) |
---|---|---|
Z | ZF=1 | 最后的操作结果为0。 |
NZ | ZF=0 | 最后的操作结果非0。 |
C | CF=1 | 最后的操作需要进位或借位。(对于无符号整型而言,这代表溢出。) |
NC | CF=0 | 最后的操作不需要进位或借位。(对于无符号整型而言,这代表溢出。) |
S | SF=1 | 最后的操作结果有高位bit集。 |
NS | SF=0 | 最后的操作结果没有高位bit集。 |
O | OF=1 | 当处理有符号整型操作是,最后的操作有上溢出或是下溢出。 |
NO | OF=0 | 当处理有符号整型操作是,最后的操作无上溢出或是下溢出。 |
条件也可以用于比较两个值。cmp 指令比较两个操作数,然后设置标记位,就像一个数减去另一个数。下面的条件可用于检查 cmp value1, value2
的结果。
Condition Name(条件名称) | Flags(标记) | Meaning after a CMP operation(CMP操作后的意思)。 |
---|---|---|
E | ZF=1 | value1==value2 |
NE | ZF=0 | value1!=value |
GE NL | SF=OF | value>=value2。这些值被当作有符号整型。 |
LE NG | ZF=1 or SF!=OF | value1<=value2。这些值被当作有符号整型。 |
G NLE | ZF=0 and SF=OF | value1>value2。这些值被当作有符号整型。 |
L NGE | SF!=OF | value1<value2。这些值被当作有符号整型。 |
AE NB | CF=0 | value1>=value2。这些值被当作无符号整型。 |
BE NA | CF=1 or ZF=1 | value1<=value2。这些值被当作无符号整型。 |
A NBE | CF=0 and ZF=0 | value1>value2。这些值被当作无符号整型。 |
B NAE | CF=1 | value1<value2。这些值被当作无符号整型。 |
条件通常用于对 cmp
或是 test
指令结果的操作。例如:
cmp eax, 5
jz equal
用表达式 (eax - 5)
来计算比较 eax 寄存器 与 5 数字,并根据结果设置对应的标记位。如果差为0,那么 zr 标记位将被设置,并且 jz 条件会为帧,因此将会跳转处理。
Data Types - 数据类型
- byte : 8 bits
- word : 16 bits
- dword : 32 bits
- qword : 64 bits(包含双精度浮点)
- tword : 80 bits(包含扩展的双精度浮点)
- owrod : 128 bits
Notation - 符号
下面表格的符号代表用于汇编语言指令的描述:
Notation | Meaning |
---|---|
r, r1, r2… | 寄存器 |
m | 内存地址(查看后续的寻址模式小节了解更多信息)。 |
#n | 立即数 |
r/m | 寄存器 或 内存 |
r/#n | 寄存器 或 立即数 |
r/m/#n | 寄存器 或 内存,或 立即数 |
cc | 之间的条件小节列表中的条件代码。 |
T | “B”, “W"或"D”(byte, word 或 dword) |
accT | Size T accumulator(累加器T的大小):如果是al T = “B”,如果是 ax T=“W”,或是如果 eax T = “D” |
Addressing Modes - 寻址模式
有一些不同的寻址模式,但他们所有采用 T ptr [expr] 的形式,T 是数据类型(查看之前的 Data Type 数据类型小节), expr 表达式涉及 常数与寄存器。
多系数的模式的符号可以不太难的就推导出来。例如,BYTE PTR [esi + edx * 8 + 3] 意思是“取 esi 寄存器的值,加上 edx 值的8倍的值,再加上3,然后以结果为地址取去一个字节。”
Pipelining - 流水线
什么是Pipelining,可以参考:2
崩腾有双工问题,这意味着它可以在一个时钟周期可以执行两个行为。然而,什么时候能同时执行两个行为(成对的行为)是非常复杂的。
因为 x86 是CISC处理器,你不需要担心 跳过延迟槽(jump delay slots) 的问题
Synchronized Memory Access - 同步的内存访问
加载、修改,与储存指令都会遇到一个 lock 修改指令的前置问题,如下:
- 在发出指令前,CPU将会flush掉所有pending的内存操作,以确保内存数据一致性。预先取到的所有数据将放弃。
- 当发出指令时,CPU将独占的访问总线。确保load(加载)/modify(修改)/store(储存)操作的原子性。
xchg 指令将自动遵循前面的规则,无论何时都会使用内存数据来调整数值。
所有其他的指令默认无锁。
Jump Prediction - 跳转预测
无条件跳转(jumps)都是可被预测。
有条件跳转的预测可能会也可能不会,依赖于他们最后一次的执行。记录跳转历史的缓存是有大小限制的。
如果CPU没有记录最后一次的条件跳转是否被执行,那么反向条件跳转将被预测执行,而正向条件跳转不会被执行。
是文档太绕还是我理解不了,-_-!!!
Alignment - 对齐
x86 处理器将自动修正没对齐的内存访问,但会有性能惩罚。也没有异常抛出。
如果对象的地址大小是整数倍的,那么这一内存访问是被认为是对齐的。例如,所有的BYTE访问都被认为是对齐的(都是1(字节)的倍数),WORD方位偶数地址是对齐的,而DWORD的地址为了对齐必须是4(个字节)的倍数。
lock 不应该用于无对齐的内存访问。
FLAT 模式 - 因为和 16 位 Windows 下的把代码分成 DATA,CODE 等段的内存模式不同,WIN32 只有一种内存模式,即 FLAT 模式, 意思是"平坦"的内存模式,再没有 64K 的段大小限制,所有的 WIN32 的应用程序运行在一个连续、平坦、巨大的 4GB 的空间中。这同时也意 味着您无须和段寄存器打交道,您可以用任意的段寄存器寻址任意的地址空间,这对于程序员来说是非常方便的。在Win32下编程,有许多重要的规则需要遵 守。有一条很重要的是:Windows 在内部频繁使用 ESI,EDI,EBP,EBX 寄存器,而且并不去检测这些寄存器的值是否被更改,这样当您要 使用这些寄存器时必须先保存它们的值,待用完后再恢复它们。 ↩︎
How Pipelining Works、计算机体系结构——流水线技术(Pipelining)、计算机体系结构——流水线技术 ↩︎