杭州做肉松饼的网站有多少家,济南做网站哪家好怎么选,百度爱采购推广平台,九一人才网找工作赣州官网本文翻译自#xff1a;Guide to x86 Assembly
在阅读 Linux 源码之前#xff0c;我们需要有一些 x86 汇编知识。本指南描述了 32 位 x86 汇编语言编程的基础知识#xff0c;包括寄存器结构#xff0c;数据表示#xff0c;基本的操作指令#xff08;包括数据传送指令、逻…本文翻译自Guide to x86 Assembly
在阅读 Linux 源码之前我们需要有一些 x86 汇编知识。本指南描述了 32 位 x86 汇编语言编程的基础知识包括寄存器结构数据表示基本的操作指令包括数据传送指令、逻辑计算指令、算数运算指令以及函数的调用规则。
一、寄存器Registers
如下图现代 x86 处理器有 8 个 32-bit 的通用寄存器 由于历史原因EAX 寄存器过去被用于算术运算ECX被用于保存循环索引。尽管现在大多数寄存器在现代指令集中已经失去了它们的特殊用途但有两个寄存器一直保留用于特殊用途——堆栈指针ESP用于指示栈顶位置和基址指针EBP用于指示子程序或函数调用的基址指针。
对于 EAX、EBX、ECX、EDX 四个寄存器来说其前两个高位字节和后两个低位字节可以独立使用而后两个低位字节又分为高位H和低位L部分。这样做的原因主要是为了兼容 16 位程序。
二、内存和寻址模式Memory and Addressing Modes
2.1 声明静态数据区Declaring Static Data Regions
我们可以使用特殊的汇编指令 .DATA 在 x86 汇编中声明静态数据区域类似于全局变量在此指令之后可以使用指令 DB、DW 和 DD分别声明一个、两个和四个字节的数据大小声明的数据可以用标签label标记以供以后引用。并且按顺序声明的数据将位于内存中相邻的位置。
下面是一个相关的例子
.DATA
var DB 64 ; 声明一个字节, 里面的值为 64
var2 DB ? ; 声明一个未初始化的字节DB 10 ; 声明一个没有 label 的字节, 其值为 10
X DW ? ; 声明一个未初始化的双字节
Y DD 30000 ; 声明一个四字节值为 30000
x86 汇编语言中的数组只是在内存中连续放置的若干单元格可以使用 DUP 指令来声明数据数组 Z DD 1, 2, 3 ; 声明三个四字节的值, 分别初始化为 1, 2, 3 Z8 位置存放的是 3
bytes DB 10 DUP(?) ; 声明 10 个未初始化的单字节数据
arr DD 100 DUP(0) ; 声明 100 个初始化为 0 的四字节数据
str DB hello,0 ; 声明 6 个单字节数据前五个被初始化为 ASCII 字符, 最后一个初始化为 null(0) 字节
2.2 内存寻址Addressing Memory
现代 x86 处理器最多能够寻址 字节大小的内存。在上面的例子中我们使用标签表示内存区域这些标签在实际汇编时均被 32 位的实际地址代替。除了支持这种直接的内存区域描述X86 还提供了一种灵活的内存寻址方式即利用最多两个 32 位的寄存器和一个 32 位的有符号常数相加计算一个内存地址其中一个寄存器可以乘 2, 4 或 8 以表述更大的空间。
下面是正确使用的例子
mov eax, [ebx] ; 将 ebx 值指示的内存地址中的 4 个字节移动到 eax 中
mov [var], ebx ; 将 ebx 的内容移动到 var 标签所指示的内存地址中
mov eax, [esi-4] ; 将 esi-4 值指示的内存地址中的 4 个字节移动到 eax 中
mov [esieax], cl ; 将 cl 值的内容移动到 esieax 所指示的内存地址中
mov edx, [esi4*ebx] ; 将 esi4*ebx 所指示的内存地址中的 4 个字节移动到 edx 中
下面是错误使用的例子
mov eax, [ebx-ecx] ; 只能将寄存器的值相加不能相减
mov [eaxesiedi], ebx ; 最多只能有两个寄存器相加通常给定内存地址中数据项的预期大小可以从引用它的汇编代码指令推断出来。当我们加载一个 32 位寄存器时汇编器可以推断我们引用的内存区域是 4 字节宽。当我们将一个字节寄存器的值存储到内存中时汇编器可以推断出我们希望地址指向内存中的一个字节。
但对于 mov [ebx], 2 这条指令来说汇编器无法判断应该将 2 值作为单个字节的数据还是多个字节的数据这时就需要用到指令 BYTE PTR、WORD PTR 和 DWORD PTR分别表示 1、2 和 4 字节的大小
mov BYTE PTR [ebx], 2 ; 将一个字节表示的 2 移动到 ebx 所指向的内存地址处
mov WORD PTR [ebx], 2 ; 将两个字节表示的 2 移动到 ebx 所指向的内存地址处
mov DWORD PTR [ebx], 2 ; 将四个字节表示的 2 移动到 ebx 所指向的内存地址处三、指令Instructions
机器指令通常分为三类数据移动指令、算术/逻辑指令和控制流指令。在本节中我们将分别介绍每种类型的重要 x86 指令示例。 这会使用到如下的几种符号表示
reg32 任意 32 位寄存器 (EAX, EBX, ECX, EDX, ESI, EDI, ESP, or EBP)reg16任意 16 位寄存器 (AX, BX, CX, or DX)reg8任意 8 位寄存器 (AH, BH, CH, DH, AL, BL, CL, or DL)reg任意寄存器mem内存地址 (e.g., [eax], [var 4], or dword ptr [eaxebx])con32任意 32 位常数con16任意 16 位常数con8任意 8 位常数con任意 32 位、16 位或 8 位常数
3.1 数据移动指令Data Movement Instructions
mov — Move (Opcodes: 88, 89, 8A, 8B, 8C, 8E, ...)
mov指令将其第二个操作数内容可以是寄存器、内存或常量复制到其第一个操作数寄存器或内存中。 不能直接用 mov 指令将内存中的值移动到另外一个内存地址。下面是 mov 指令的语法
mov reg,reg
mov reg,mem
mov mem,reg
mov reg,const
mov mem,const
例子
mov eax, ebx ; 复制 ebx 中的内容到 eax
mov byte ptr [var], 5 ; 将单字节表示的 5 存入 var 所指示的内存单元
push — Push stack (Opcodes: FF, 89, 8A, 8B, 8C, 8E, ...)
push 指令操作数压入内存的栈中。具体来说push 指令首先将 ESP (栈指针) 减 4因为 x86 栈向下增长然后将其操作数放入地址为 [ESP] 位置的内存单元中。用法如下
push reg32
push mem
push con32
例子
push eax — push eax on the stack
push [var] — push the 4 bytes at address var onto the stack
pop — Pop stack
pop 指令从栈中取出一个数据放入指定寄存器或内存中。pop 指令首先移动地址为 [ESP] 位置的内存单元中的数据到指定寄存器或内存然后再将 ESP 加 4。用法如下
pop reg32
pop mem
例子
pop edi — pop the top element of the stack into EDI.
pop [ebx] — pop the top element of the stack into memory at the four bytes starting at location EBX.
lea — Load effective address
lea 指令将第二个操作数指定的内存地址放置到第一个操作数指定的寄存器中。该指令不会加载该内存位置的内容只计算有效地址并放入寄存器。用法如下
lea reg32,mem
例子
lea edi, [ebx4*esi] — the quantity EBX4*ESI is placed in EDI.
lea eax, [var] — the value in var is placed in EAX.
lea eax, [val] — the value val is placed in EAX.
3.2 算术/逻辑指令Arithmetic and Logic Instructions
add — Integer Addition
add 指令将两个操作数相加结果存储到第一个操作数中。用法如下
add reg,reg
add reg,mem
add mem,reg
add reg,con
add mem,con
例子
add eax, 10 — EAX ← EAX 10
add BYTE PTR [var], 10 — add 10 to the single byte stored at memory address var
sub — Integer Subtraction
sub 指令将两个操作数相减结果存储到第一个操作数中。用法如下
sub reg,reg
sub reg,mem
sub mem,reg
sub reg,con
sub mem,con
例子
sub al, ah — AL ← AL - AH
sub eax, 216 — subtract 216 from the value stored in EAX
inc, dec — Increment, Decrement
inc 指令将操作数的内容加一而 dec 指令将操作数的内容减一。用法如下
inc reg
inc mem
dec reg
dec mem
例子
dec eax — subtract one from the contents of EAX.
inc DWORD PTR [var] — add one to the 32-bit integer stored at location var
imul — Integer Multiplication
imul 指令分为两个操作数和三个操作数两种类型如果是两个操作数则让两个操作数相乘并将结果存入第一个操作数中如果是三个操作数则让第二个和第三个操作数相乘结果存入第一个操作数中且第三个操作数必须是常数。不管哪种类型第一个操作数必须是寄存器。用法如下
imul reg32,reg32
imul reg32,mem
imul reg32,reg32,con
imul reg32,mem,con
例子
imul eax, [var] — multiply the contents of EAX by the 32-bit contents of the memory location var. Store the result in EAX.
imul esi, edi, 25 — ESI → EDI * 25
idiv — Integer Division
idiv 指令将 64 位整数 EDX:EAX (将 EDX 视为高 4 字节将 EAX 视为低 4 字节) 的内容除以指定的操作数值。除法的商存储在 EAX 中余数存储在 EDX 中。 用法如下
idiv ebx — divide the contents of EDX:EAX by the contents of EBX. Place the quotient in EAX and the remainder in EDX.
idiv DWORD PTR [var] — divide the contents of EDX:EAX by the 32-bit value stored at memory location var. Place the quotient in EAX and the remainder in EDX.
and, or, xor — Bitwise logical and, or and exclusive or
andorxor 指令分别对两个操作数作与、或、异或操作结果存入第一个操作数中。用法如下
and reg,reg
and reg,mem
and mem,reg
and reg,con
and mem,conor reg,reg
or reg,mem
or mem,reg
or reg,con
or mem,conxor reg,reg
xor reg,mem
xor mem,reg
xor reg,con
xor mem,con
例子
and eax, 0fH — clear all but the last 4 bits of EAX.
xor edx, edx — set the contents of EDX to zero.
not — Bitwise Logical Not
not 指令逻辑上对操作数的内容求反 (即翻转操作数中的所有位)。用法如下
not reg
not mem
neg — Negate
neg 指令对操作数内容取负。用法如下
neg reg
neg memExample
neg eax — EAX → - EAX
shl, shr — Shift Left, Shift Right
shl, shr 指令会左右移动第一个操作数的位用0填充空的位。操作数最多可以被移动 31 位。移位的位数由第二个操作数指定若操作数大于 32则对其求模。 用法如下
shl reg,con8
shl mem,con8
shl reg,cl
shl mem,clshr reg,con8
shr mem,con8
shr reg,cl
shr mem,cl
3.3 控制流指令Control Flow Instructions
x86 处理器维持着一个指示当前执行指令的指令指针IP当一条指令执行后此指针自动指向下一条指令。IP 寄存器不能直接操作但是可以用控制流指令更新CS和IP寄存器的作用及执行分析_cs ip_猪哥-嵌入式的博客-CSDN博客。
一般用标签label指示程序中的地址在 x86 汇编代码中可以在任何指令前加入标签。如 mov esi, [ebp8]
begin: xor ecx, ecxmov eax, [esi]
这种标签只是用于取代 32 位地址值的一种便利的表示方式。这样在其他代码中就可以使用该标签而不是使用其对应具体的 32 位地址。
jmp — Jump
jmp 指令用于跳转到指定的 label 位置处执行。用法如下
jmp labelExample
jmp begin — Jump to the instruction labeled begin.
jcondition — Conditional Jump
jcondition 类的指令用于条件跳转即满足条件才跳转。这些条件存储在一个称为机器状态字 (machine status word) 的特殊寄存器中。一般会在执行下列指令之前先执行 cmp 指令对两个操作数进行比较。用法如下
je label (jump when equal)
jne label (jump when not equal)
jz label (jump when last result was zero)
jg label (jump when greater than)
jge label (jump when greater than or equal to)
jl label (jump when less than)
jle label (jump when less than or equal to)Example
cmp eax, ebx
jle done; 如果 eax 小于 ebx 的内容则跳转到标签 done 所指向的位置处指向若大于则继续往下执行
cmp — Compare
cmp 指令比较两个操作数指定的值然后设置适当的机器状态字的状态码。这条指令等同于减法指令但是会舍弃减法的结果。用法如下
cmp reg,reg
cmp reg,mem
cmp mem,reg
cmp reg,conExample
cmp DWORD PTR [var], 10
jeq loop
call, ret — Subroutine call and return
call 和 ret 指令用于实现子程序的调用和返回。
call 指令首先将当前代码执行位置压入栈中然后无条件跳转到 label 操作数指定的位置。与简单的跳转指令不同该指令会保存当前代码执行位置以便子程序执行完后返回。
ret 指令实现子程序的返回。该指令首先从栈弹出 call 指令保存的代码执行位置然后无条件返回到该位置继续执行。
call label
ret
四、调用约定Calling Convention
为了加强程序员之间的协作及简化程序开发进程设定一个函数调用约定非常必要函数调用约定规定了函数调用及返回的规则只要遵照这种规则写的程序均可以正确执行从而程序员不必关心诸如参数如何传递等问题另一方面在汇编语言中可以调用符合这种规则的高级语言所写的函数从而将汇编语言程序与高级语言程序有机结合在一起。有了这个约定我们就可以在汇编代码中调用 C 函数或者在 C 代码中调用汇编实现的函数。
调用约定分为两个方面及调用者约定和被调用者约定如一个函数 A 调用一个函数 B则 A 被称为调用者 (Caller)B 被称为被调用者 (Callee)。
下图显示一个调用过程中的内存中的栈布局 4.1 Caller Rules
调用者规则包括一系列约定
在调用子程序之前调用者应该保存一系列被设计为调用者应该保存的寄存器的值。调用者应该保存的寄存器有 EAX、ECX、EDX。由于被调用的子程序允许修改这些寄存器如果调用者在子程序返回后还需要用到这些寄存器的值则调用者必须将这些寄存器中的值压入堆栈(以便在子例程返回后可以恢复它们)。 要将参数传递给子例程需要在调用之前将它们压入栈。参数应该按倒序推送 (即最后一个参数先 push)因为栈是向下增长的。使用 call 指令调用子程序。该指令将返回地址放在栈的顶部并转到子程序执行子程序的执行将按照被调用者的规则执行。
在子例程返回之后调用者可以期望在寄存器 EAX 中找到子程序的返回值。为了恢复调用子程序执行之前的状态调用者应该执行以下操作
清除栈中的参数。将栈中保存的 EAX 值、ECX 值以及 EDX 值出栈恢复 EAX、ECX、EDX 的值当然如果其它寄存器在调用之前需要保存也需要完成类似入栈和出栈操作。
例子
push [var] ; Push last parameter first
push 216 ; Push the second parameter
push eax ; Push first parameter lastcall _myFunc ; Call the function (assume C naming)add esp, 12
上述例子展示了一个调用者执行的操作在调用 _myFunc 函数之前先保存函数的三个传入参数 eax、216 和 var然后使用 call 指令调用函数返回后再将 esp栈指针的值加 12三个参数每个四字节加 12 的目的是为了清空栈中的三个参数。
_myFunc 的返回值保存在 eax 中。若该函数会改变 ecx 和 edx 中的值调用者还必须在调用之前将其也保存在栈中并在调用结束之后出栈恢复ecx和edx的值。
4.2 Callee Rules
将 EBP基址寄存器入栈push ebp并将 ESP 中的值拷贝到 EBP 中mov ebp esp目的是保存调用子程序之前的基址指针基址指针用于寻找栈上的参数和局部变量。当一个子程序开始执行时基址指针保存栈指针指示子程序的执行。为了在子程序完成之后调用者能正确定位调用者的参数和局部变量ebp的值需要返回。在栈上为局部变量分配空间。保存 callee-saved 寄存器的值callee-saved 寄存器包括 ebx,edi 和 esi将 ebx,edi 和 esi 压栈。
在上述三个步骤完成之后子程序开始执行当子程序返回时必须完成如下工作
将返回的执行结果保存在 eax 中弹出栈中保存的 callee-saved 寄存器值恢复 callee-saved 寄存器的值ESI和EDI收回局部变量的内存空间。实际处理时通过改变 EBP 的值即可mov esp, ebp。 通过弹出栈中保存的 ebp 值恢复调用者的基址寄存器值。执行 ret 指令返回到调用者程序。
例子
.486
.MODEL FLAT
.CODE
PUBLIC _myFunc
_myFunc PROC; Subroutine Prologuepush ebp ; Save the old base pointer value.mov ebp, esp ; Set the new base pointer value.sub esp, 4 ; Make room for one 4-byte local variable.push edi ; Save the values of registers that the functionpush esi ; will modify. This function uses EDI and ESI.; (no need to save EBX, EBP, or ESP); Subroutine Bodymov eax, [ebp8] ; Move value of parameter 1 into EAXmov esi, [ebp12] ; Move value of parameter 2 into ESImov edi, [ebp16] ; Move value of parameter 3 into EDImov [ebp-4], edi ; Move EDI into the local variableadd [ebp-4], esi ; Add ESI into the local variableadd eax, [ebp-4] ; Add the contents of the local variable; into EAX (final result); Subroutine Epilogue pop esi ; Recover register valuespop edimov esp, ebp ; Deallocate local variablespop ebp ; Restore the callers base pointer valueret
_myFunc ENDP
END
子程序首先通过入栈的手段保存 ebp分配局部变量保存寄存器的值。
在子程序体中参数和局部变量均是通过 ebp 进行计算。由于参数传递在子程序被调用之前所以参数总是在 ebp 指示的地址的下方在栈中因此上例中的第一个参数的地址是 ebp8第二个参数的地址是 ebp12第三个参数的地址是 ebp16而局部变量在 ebp 指示的地址的上方所有第一个局部变量的地址是 ebp-4而第二个这是 ebp-8.