内容简介:3.3. 取址模式16位代码使用分段内存模式。一个内存操作数可以有任意这些部分:一个内存操作数可以有所有这些部分。一个仅包含立即数偏移的操作数不能被MASM解释为一个内存操作数,即使它有一个[]。例如:
3.3. 取址模式
16 位模式中的取址
16位代码使用分段内存模式。一个内存操作数可以有任意这些部分:
- 段说明。这可以是任何段寄存器或与一个段寄存器关联的一个段或组名。(缺省的段是DS,除非BP被用作一个基址寄存器)。段可由一个段里定义的标签暗示。
- 一个定义可重定位偏移的标签。这个偏移相对于由链接器计算的段起始地址。
- 一个立即数偏移。这是一个常量。如果还有一个重定位偏移,这些值加起来。
- 一个基址寄存器。这只能是BX或BP。
- 一个索引寄存器。这只能是SI或DI。可以没有比例因子。
一个内存操作数可以有所有这些部分。一个仅包含立即数偏移的操作数不能被MASM解释为一个内存操作数,即使它有一个[]。例如:
; Example 3.3. Memory operands in 16-bit mode
MOV AX, DS:[100H] ; Address has segment and immediate offset
ADD AX, MEM[SI]+4 ; Has relocatable offset and index and immediate
比64 kb大的数据结构以下面的方式处理。在实模式或虚拟模式(DOS)中:向段寄存器加1对应偏移加10H。在保护模式(Windows 3.x)中:向段寄存器加8,对应偏移加10000H。加到段的值必须是8的倍数。
32 位模式中的取址
在大多数情形下,32位代码使用一个扁平内存模型。分段是可能的,但仅用于特殊目的(即FS中的线程环境块)。
一个内存操作数可以由任意这些组成:
- 一个段说明。不用在扁平模式中。
- 一个定义了重定位偏移的标签。FLAT段组相关的标签由链接器计算。
- 一个立即偏移。这是一个常量。如果还有一个重定位偏移,那么这些值加起来。
- 一个基址寄存器。这可以是任意32位寄存器。
- 一个索引寄存器。这可以是任意32位寄存器,除了ESP。
- 一个应用于索引寄存器的比例因子。允许值有1,2,4,8。
一个内存操作数可以有所有这些部分。例如:
; Example 3.4. Memory operands in 32-bit mode
mov eax, fs:[10H] ; Address has segment and immediate offset
add eax, mem[esi] ; Has relocatable offset and index
add eax, [esp+ecx*4+8] ; Base, index, scale and immediate offset
32 位模式中的位置无关代码
在32位类Unix系统中,制作共享库(*.so)要求位置无关代码。在32位 Linux 与BSD中制作位置无关代码的常用方法,是使用包含所有静态对象地址的全局偏移表(GOT)。GOT方法相当低效,因为每次在代码段中读或写数据时,代码必须从GOT获取一个地址。一个更快的方法是使用一个任意援引点,如下面例子所示:
; Example 3.5a. Position-independent code, 32 bit, YASM syntax
SECTION .data
alpha: dd 1
beta: dd 2
SECTION .text
funca: ; This function returns alpha + beta
call get_thunk_ecx ; get ecx = eip
refpoint: ; ecx points here
mov eax, [ecx+alpha-refpoint] ; relative address
add eax, [ecx+beta -refpoint] ; relative address
ret
get_thunk_ecx: ; Function for reading instruction pointer
mov ecx, [esp]
ret
在32位模式中唯一读指令指针的是call指令。在例子3.5中,我们使用call get_thunk_ecx将指令指针(eip)读入ecx。那么ecx将指向该调用后的第一条指令。这是我们的援引点,称为refpoint。
用于32位Mac OS X的Gnu编译器使用这个方法的一个略有差异的版本:
Example 3.5b. Bad method!
funca: ; This function returns alpha + beta
call refpoint ; get eip on stack
refpoint:
pop ecx ; pop eip from stack
mov eax, [ecx+alpha-refpoint] ; relative address
add eax, [ecx+beta -refpoint] ; relative address
ret
在例子3.5b中使用的方法是不好的,因为它有一条没有被return匹配的call指令。这将导致后续return被误预测。(return预测的解释,参考手册3《Intel,AMD与VIA CPU微架构》)。
这个方法广泛用在Mac系统中,其中mach-o文件格式支持相对一个任意点的引用。其他文件格式不支持这类引用,但使用带有一个偏移的自相关引用是可能的。YASM与Gnu汇编器将自动完成这,而大多数其他汇编器不能处理这个情形。因此,如果希望以这个方法在32位模式中产生位置无关代码,使用YASM或Gnu汇编器是必要的。在调试器或反汇编器中,代码看起来可能奇怪,但在32位Linux,BSD与Windows系统中,它的执行没有问题。在32位Mac系统中。载入器不能正确识别目标的节,除非你使用一个支持引用点方法的汇编器(我不知道Gnu汇编器以外,其他任何可以正确执行的汇编器)。
GOT方法使用与例子3.5a相同的引用点方法来访问GOT,然后使用GOT将alpha与beta的地址读入其他指针寄存器。这是不必要的时间与寄存器的浪费,因为如果我们可以相对于引用点访问GOT,那么我们也可以相对于引用点访问alpha与beta。
在switch/case语句中使用的指针表可以将相同的引用点用于表以及表中的指针:
; Example 3.6. Position-independent switch, 32 bit, YASM syntax
SECTION .data
jumptable: dd case1-refpoint, case2-refpoint, case3-refpoint
SECTION .text
funcb: ; This function implements a switch statement
mov eax, [esp+4] ; function parameter
call get_thunk_ecx ; get ecx = eip
refpoint: ; ecx points here
cmp eax, 3
jnb case_default ; index out of range
mov eax, [ecx+eax*4+jumptable-refpoint] ; read table entry
; The jump addresses are relative to refpoint, get absolute address:
add eax, ecx
jmp eax ; jump to desired case
case1: ...
ret
case2: ...
ret
case3: ...
ret
case_default:
...
ret
get_thunk_ecx: ; Function for reading instruction pointer
mov ecx, [esp]
ret
64 位模式中的取址
64位代码总是使用一个扁平内存模型。分段是不可能的,除了仅用于特殊目的的FS与GS(线程环境块等)。
在64位模式中,有几个不同的取址模式:RIP相对,32位绝对,64位绝对及基址寄存器相对。
RIP 相对取址
这是静态数据优先使用的取址模式。地址包含一个相对于指令指针的32位符号扩展偏移。地址不能包含任何段寄存器或索引寄存器,除隐含的RIP外其他基址寄存器。例如:
; Example 3.7a. RIP-relative memory operand, MASM syntax
mov eax, [mem]
; Example 3.7b. RIP-relative memory operand, NASM/YASM syntax
default rel
mov eax, [mem]
; Example 3.7c. RIP-relative memory operand, Gas/Intel syntax
mov eax, [mem+rip]
在没有显式说明基址或索引寄存器时,MASM汇编器总是对静态数据产生RIP取址。在其他汇编器上,你必须记住要说明相对取址。
64 位模式中的32 位绝对取址
32位常量地址被符号扩展到64位。仅在确认所有的地址都在231以下(或对系统代码在-231之上)时,这个取址模式才可使用。
在Linux与BSD主可执行文件中,使用32位绝对取址是安全的,其中使用的地址缺省都在231以下,但它不能用在共享对象中。32位绝对地址通常也能用在Windows主执行文件中(但DLL不行),但没有Windows编译器使用这个可能性。
不能在Mac OS X中使用32位绝对地址,其中的地址缺省都在232之上。
注意在你没有显式说明rip相对地址时,NASM,YASM与Gnu汇编器会制作32位绝对地址。你必须在NASM/YASM中声明defaul rel,在Gas中声明[mem+rip],来避免32位绝对地址。
对简单的内存操作数,使用绝对地址完全没理由。Rip相对地址使指令更短,它们消除了载入时的重定位需要,并且在所有的系统中它们都是安全的。
仅对访问有一个索引寄存器的数组时,绝对地址才是需要的,即。
; Example 3.8. 32 bit absolute addresses in 64-bit mode
mov al, [chararray + rsi]
mov ebx, [intarray + rsi*4]
仅当保证地址 < 231时,才可使用这个方法,如上解释。参考下面静态数组取址的替代方法。
像上面例子3.8那样,仅在一起指定一个基址或索引寄存器与一个内存标签时,MASM汇编器才产生绝对地址。
索引寄存器最好是一个64位寄存器,不是32位寄存器。分段只可能使用FS或GS。
64 位绝对取址
这使用64位绝对虚拟地址。这个地址不能包含任何段寄存器、基址或索引寄存器。64位绝对地址仅可用于MOV指令,且仅能使用AL,AX,EAX或RAX作为源或目标。
; Example 3.9. 64 bit absolute address, YASM/NASM syntax
mov eax, dword [qword a]
MASM汇编器不支持这个取址模式,但其他汇编器支持。
相对64 位基址寄存器的取址
这个模式中的一个内存操作数可以有任意这些部分:
- 一个基址寄存器。这可以是任意64位整数寄存器。
- 一个索引寄存器。这可以是任意64位整数寄存器,除了RSP。
- 一个适用于索引寄存器的比例因子。可能值有1,2,4,8。
- 一个立即数偏移。这是一个相对基址寄存器的偏移常量。
对这个取址模式,总是需要一个基址寄存器。其他部分是可选的。例如:
; Example 3.10. Base register addressing in 64 bit mode
mov eax, [rsi]
add eax, [rsp + 4*rcx + 8]
这个取址模式用于栈上数据、结构体与类成员,以及数组。
64 位模式中的静态数组取址
使用RIP相对取址与一个索引寄存器访问静态数组是不可能。有几个可能的替代方法。
下面的例子对静态数组取址。这个例子的C++代码是:
// Example 3.11a. Static arrays in 64 bit mode
// C++ code:
static int a[100], b[100];
for (int i = 0; i < 100; i++) {
b[i] = -a[i];
}
最简单的解决方案是使用32位绝对地址。只要地址在231以下,这是可行的。
; Example 3.11b. Use 32-bit absolute addresses
; (64 bit Linux)
; Assumes that image base < 80000000H
.data
A DD 100 dup (?) ; Define static array A
B DD 100 dup (?) ; Define static array B
.code
xor ecx, ecx ; i = 0
TOPOFLOOP: ; Top of loop
mov eax, [A+rcx*4] ; 32-bit address + scaled index
neg eax
mov [B+rcx*4], eax ; 32-bit address + scaled index
add ecx, 1
cmp ecx, 100 ; i < 100
jb TOPOFLOOP ; Loop
汇编器将对例子3.11b中的A与B产生一个32位可重定位地址,因为它不能将一个RIP相对地址与一个索引寄存器合并。
64位Linux中的Gnu与Intel编译器使用这个方法来访问静态数组。我看到64位Windows没有编译器使用这个方法,但如果地址小于231,它在Windows上也能工作。对应用程序,映像的基址通常是222,对DLL在228与229间,因此这个方法在大多数情形里都工作,但不是所有。这个方法通常不能用在64位Mac系统中,因为所有地址缺省都超过232。
第二个方法是所有映像相对取址。以下解决方案通过使用带有一个RIP相对地址的LEA指令,将映像基址载入寄存器RBX:
; Example 3.11c. Address relative to image base
; 64 bit, Windows only, MASM assembler
.data
A DD 100 dup (?)
B DD 100 dup (?)
extern __ImageBase:byte
.code
lea rbx, __ImageBase ; Use RIP-relative address of image base
xor ecx, ecx ; i = 0
TOPOFLOOP: ; Top of loop
; imagerel(A) = address of A relative to image base:
mov eax, [(imagerel A) + rbx + rcx*4]
neg eax
mov [(imagerel B) + rbx + rcx*4], eax
add ecx, 1
cmp ecx, 100
jb TOPOFLOOP
这个方法仅用在64位Windows中。在Linux中,映像基址可通过__executable_start得到,但映像相对地址在ELF文件格式中不支持。Mach-O格式允许地址相对于一个任意引用点,包括可通过__mh_execute_header得到的映像基址。
第三个解决方案是通过带有一个RIP相对地址的LEA指令,将数组A的地址载入寄存器RBX。相对于A,计算B的地址。
; Example 3.11d.
; Load address of array into base register
; (All 64-bit systems)
.data
A DD 100 dup (?)
B DD 100 dup (?)
.code
lea rbx, [A] ; Load RIP-relative address of A
xor ecx, ecx ; i = 0
TOPOFLOOP: ; Top of loop
mov eax, [rbx + 4*rcx] ; A[i]
neg eax
mov [(B-A) + rbx + 4*rcx], eax ; Use offset of B relative to A
add ecx, 1
cmp ecx, 100
jb TOPOFLOOP
注意,对递增的索引(ADD ECX, 1),我们可以使用一条32位指令,即使我们正在对索引(RCX)使用64位寄存器。这可行,因为我们确保索引不是负的且小于232。这个方法可以将数据段中的任意地址用作引用点,并相对于这个引用点计算其他地址。
如果数组离开指令指针超过231字节,那么我们必须将整个64位地址载入基址寄存器。例如,在例子3.11的中,我们可以MOV RBX, OFFSET A替换LEA RBX, [A]。
64 位模式中的位置无关代码
在64位模式中,位置无关代码很容易制作。静态数据可通过rip相对取址访问。静态数组可如例子3.11d般访问。
可相对于任意一个引用点,制作switch语句的指针表。使用表本身作为引用点是方便的:
; Example 3.12. switch with relative pointers, 64 bit, YASM syntax
SECTION .data
jumptable: dd case1-jumptable, case2-jumptable, case3-jumptable
SECTION .text
default rel ; use relative addresses
funcb: ; This function implements a switch statement
mov eax, [rsp+8] ; function parameter
cmp eax, 3
jnb case_default ; index out of range
lea rdx, [jumptable] ; address of table
movsxd rax, dword [rdx+rax*4] ; read table entry
; The jump addresses are relative to jumptable, get absolute address:
add rax, rdx
jmp rax ; jump to desired case
case1: ...
ret
case2: ...
ret
case3: ...
ret
case_default:
...
ret
这个方法有助于减小长指针表的大小,因为它使用32位相对指针,而不是64位绝对指针。
MASM汇编器不能产生例子3.12中的相对表,除非该跳转表放在代码段里。为了优化缓存与代码预取,最好把跳转表放在数据段中,YASM或Gnu汇编器可以这样做。
3.4. 指令代码格式
指令代码的格式在Intel与AMD的手册中详细描述。指令编码的基本原则在这里解释,因为它与微处理器性能相关。通常,你可以依赖汇编器来产生一条指令最小的可能编码。
每条指令可以包含以下元素,按提及的顺序:
- 前缀(0 ~ 5字节)
这些是修改随后操作码(opcode)含义的前缀。有几种前缀,在下面表3.12中描述。
- 操作码(1 ~ 3字节)
这是指令代码。它可以是这些形式:
单字节:XX
双字节:0F XX
三字节:0F 38 XX或0F 3A XX
0F 38 XX形式的三字节操作码总是有一个mod-reg-r/m字节而没有位移(displacement)。0F 3A XX形式的三字节操作码总是一个mod-reg-r/m字节与1字节位移。
- Mod-reg-r/m字节(0 ~ 1字节)
这个字节说明操作数。它包含三个域。Mod域是说明取址模式的两个比特,reg域是说明用于第一个操作数(通常目标操作数)寄存器的三个比特,r/m域是说明第二个操作数(通常源操作数)的三个比特,这个操作数可以是寄存器或内存操作数。如果仅有一个操作数,reg域可以是操作码的一部分。
- SIB字节(0 ~ 1字节)
这个字节用于带有复杂取址模式的内存操作数,且仅当存在一个mod-reg-r/m字节。它有两个比特用于比例因子,三个比特说明一个比例索引寄存器,三个比特说明一个基准指针寄存器。在以下情形下,需要一个SIB字节:
- 如果一个内存操作数有两个指针或索引寄存器,
- 如果一个内存操作数有一个比例索引寄存器,
- 如果一个内存操作数有用作基址指针的栈指针(ESP或RSP),
- 如果一个64位模式中的内存操作数使用一个32位符号扩展直接内存地址,而不是一个RIP相对地址。SIB字节不能用在16位取址模式里。
- 位移(displacement,0,1,2,4或8字节)
这是一个内存操作数地址的一部分。指针寄存器(基址或索引或两者)的值,如果有,加上它。如果声明一个指针寄存器,1字节符号扩展位移在所有取址模式中都是合适的。2字节位移仅在16位取址模式是合适的。4字节位移在32位取址模式中是合适的。4字节符号扩展位移在64位取址模式中是合适的。如果声明了任意指针寄存器,那么它们加上这个位移。如果没有声明指针寄存器,也没有SIB字节,那么位移加到RIP上。如果有一个SIB字节,但没有指针寄存器,那么符号扩展值是一个绝对的直接地址。
在64位取址模式中,对少数没有mod-reg-r/m字节的MOV指令,一个8字节绝对直接地址是合适的。
- 立即操作数(0,1,2,4或8字节)
这是一个数据常量,在大多数情形里,是操作的一个源操作数。对所有可以有立即数的指令,除了MOV,CALL与RET,在使用模式中,1字节符号扩展立即数都是合适的。2字节立即数对16比特操作数大小的指令是合适的。4字节立即数对32比特操作数大小指令是合适的。4字节符号扩展立即数对64比特操作数大小指令是合适的。8字节立即数仅对移入一个64位寄存器是合适。
3.4. 指令前缀
下表汇总指令前缀的使用。
前缀 |
16 位模式 |
32 位模式 |
64 位模式 |
8 比特操作数大小 |
none |
none |
none |
16 比特操作数大小 |
none |
66h |
66h |
32 比特操作数大小 |
66h |
none |
none |
64 比特操作数大小 |
n.a. |
n.a. |
REX.W (48h) |
在 mmx 寄存器中封装整数 |
none |
none |
none |
在 xmm 寄存器中封装整数 |
66h |
66h |
66h |
在 xmm 寄存器中封装单精度浮点 |
none |
none |
none |
在 xmm 寄存器中封装双精度浮点 |
66h |
66h |
66h |
在 xmm 寄存器中封装单精度浮点标量 |
F3h |
F3h |
F3h |
在 xmm 寄存器中封装双精度浮点标量 |
F2h |
F2h |
F2h |
16 位地址大小 |
none |
67h |
n.a. |
32 位地址大小 |
67h |
none |
67h |
64 位地址大小 |
n.a. |
n.a. |
none |
CS 段 |
2Eh |
2Eh |
n.a. |
DS 段 |
3Eh |
3Eh |
n.a. |
ES 段 |
26h |
26h |
n.a. |
SS 段 |
36h |
36h |
n.a. |
FS 段 |
64h |
64h |
64h |
GS 段 |
65h |
65h |
65h |
REP 或 REPE 字符串操作 |
F3h |
F3h |
F3h |
REPNE 字符串操作 |
F2h |
F2h |
F2h |
锁定的内存操作数 |
F0h |
F0h |
F0h |
Reg 域中寄存器 R8 - R15 , XMM8 - XMM15 |
n.a. |
n.a. |
REX.R (44h) |
r/m 域中寄存器 R8 - R15 , XMM8 - XMM15 |
n.a. |
n.a. |
REX.B (41h) |
SIB.base 域中寄存器 R8 - R15 |
n.a. |
n.a. |
REX.B (41h) |
SIB.index 域中寄存器 R8 - R15 |
n.a. |
n.a. |
REX.X (42h) |
寄存器 SIL , DIL , BPL , SPL |
n.a. |
n.a. |
REX (40h) |
预测分支被采用(仅 Intel NetBurst ) |
3Eh |
3Eh |
3Eh |
预测分支不采用(仅 Intel NetBurst ) |
2Eh |
2Eh |
2Eh |
跳转上保留边界寄存器( MPX) ) |
F2h |
F2h |
F2h |
VEX 前缀, 2 字节 |
C5h |
C5h |
C5h |
VEX 前缀, 3 字节 |
C4h |
C4h |
C4h |
XOP 前缀, 3 字节(仅 AMD ) |
8Fh |
8Fh |
8Fh |
EVEX 前缀, 4 字节( AVX-512 ) |
62h |
62h |
62h |
MVEX 前缀, 4 字节(仅 Intel Knights Corner ) |
n.a. |
62h |
62h |
表3.12. 指令前缀
在扁平内存模型中,很少需要段前缀。仅在内存操作数有基址寄存器BP,EBP或ESP时,才需要DS段前缀,且DS段比SS更被期望。
锁前缀仅在读、修改或写一个内存操作数的特定指令上允许。
分支预测前缀仅工作在Intel NetBurst(Pentium 4)上,且很少需要。
可以有不超过一个REX前缀。如果需要多个REX前缀,那么这些值被OR为单个字节,值的范围在40h到4Fh。这些前缀仅在64位模式中可用。在16位与32位模式中,字节40h到4Fh都是指令代码。这些指令(INC r与DEC r)在64位模式中,有不同的编码。
前缀可以任意次序插入,除了REX前缀及多字节前缀(VEX,XOP,EVEX,MVEX),它们必须跟在其他前缀后面。
AVX指令集使用称为VEX前缀的2个与3个字节前缀。VEX前缀包括代替所有66,F2,F3及REX前缀以及多字节操作码的0F,0F 38与0F 3A转义字节的比特。VEX前缀还包括用于说明YMM寄存器、一个额外寄存器操作数的比特,及用于将来扩展比特。EVEX与MVEX前缀类似于VEX前缀,带有支持更多寄存器、掩蔽操作及其他特性的额外比特。在VEX,EVEX或MVEX前缀后,不允许有前缀。允许在VEX,EVEX或MEVX前缀前的前缀,仅有段前缀与地址大小前缀。
无意义、重复或放错位置的前缀被忽略,除了LOCK与VEX前缀。但在一个特定上下文中没有作用的前缀,在将来的处理器中可能会有影响。
不必要的前缀可替代NOP,用来对齐代码,但在某些处理器上,过多的前缀会影响指令解码。
可以有任意数量的前缀,只要指令总长度不超过15字节。例如,带有10个ES段前缀的MOV EAX, EBX将仍然可以工作,但需要更长时间来解码。
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- iOS汇编入门教程(一)ARM64汇编基础
- iOS 汇编入门教程(一):ARM64 汇编基础
- iOS汇编入门教程(三)汇编中的 Section 与数据存取
- iOS汇编入门教程(二)在Xcode工程中嵌入汇编代码
- 汇编语言8086笔记
- python编程(反汇编)
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。