[译]优化汇编例程(3)

栏目: 编程语言 · 发布时间: 4年前

内容简介: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的手册中详细描述。指令编码的基本原则在这里解释,因为它与微处理器性能相关。通常,你可以依赖汇编器来产生一条指令最小的可能编码。

每条指令可以包含以下元素,按提及的顺序:

  1. 前缀(0 ~ 5字节)

这些是修改随后操作码(opcode)含义的前缀。有几种前缀,在下面表3.12中描述。

  1. 操作码(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字节位移。

  1. Mod-reg-r/m字节(0 ~ 1字节)

这个字节说明操作数。它包含三个域。Mod域是说明取址模式的两个比特,reg域是说明用于第一个操作数(通常目标操作数)寄存器的三个比特,r/m域是说明第二个操作数(通常源操作数)的三个比特,这个操作数可以是寄存器或内存操作数。如果仅有一个操作数,reg域可以是操作码的一部分。

  1. SIB字节(0 ~ 1字节)

这个字节用于带有复杂取址模式的内存操作数,且仅当存在一个mod-reg-r/m字节。它有两个比特用于比例因子,三个比特说明一个比例索引寄存器,三个比特说明一个基准指针寄存器。在以下情形下,需要一个SIB字节:

  1. 如果一个内存操作数有两个指针或索引寄存器,
  2. 如果一个内存操作数有一个比例索引寄存器,
  3. 如果一个内存操作数有用作基址指针的栈指针(ESP或RSP),
  4. 如果一个64位模式中的内存操作数使用一个32位符号扩展直接内存地址,而不是一个RIP相对地址。SIB字节不能用在16位取址模式里。
  1. 位移(displacement,0,1,2,4或8字节)

这是一个内存操作数地址的一部分。指针寄存器(基址或索引或两者)的值,如果有,加上它。如果声明一个指针寄存器,1字节符号扩展位移在所有取址模式中都是合适的。2字节位移仅在16位取址模式是合适的。4字节位移在32位取址模式中是合适的。4字节符号扩展位移在64位取址模式中是合适的。如果声明了任意指针寄存器,那么它们加上这个位移。如果没有声明指针寄存器,也没有SIB字节,那么位移加到RIP上。如果有一个SIB字节,但没有指针寄存器,那么符号扩展值是一个绝对的直接地址。

在64位取址模式中,对少数没有mod-reg-r/m字节的MOV指令,一个8字节绝对直接地址是合适的。

  1. 立即操作数(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将仍然可以工作,但需要更长时间来解码。


以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

打造Facebook

打造Facebook

王淮、祝文让 / 印刷工业出版社 / 2013-2-1 / 39.80元

《打造Facebook》新书发布会,王淮与读者面对面,活动链接:http://www.douban.com/event/18166913/ 这本书的书名——《打造Facebook:亲历Facebook爆发的5年》很嚣张,谁有资格可以说这句话呢,当然,扎克伯格最有资格,但他不会亲自来告诉你,至少从目前的情况来看,近几年都不大可能。而且,这不是一个人的公司。里面的每一人,尤其是工程师,既是公司文......一起来看看 《打造Facebook》 这本书的介绍吧!

HTML 压缩/解压工具
HTML 压缩/解压工具

在线压缩/解压 HTML 代码

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

HEX HSV 转换工具
HEX HSV 转换工具

HEX HSV 互换工具