本节将围绕这个例子进行讲解:
#include <stdio.h>
void f (int a)
{
switch (a)
{
case 0: printf ("zero\n"); break;
case 1: printf ("one\n"); break;
case 2: printf ("two\n"); break;
default: printf ("something unknown\n"); break;
};
};
int main()
{
f(2); //test
};
Non-optimizing MSVC
使用MSVC 2010编译上述程序,可得到如下指令。
指令清单13.1 MSVC 2010
tv64 = -4 ; size = 4
_a$ = 8 ; size = 4
_f PROC
push ebp
mov ebp, esp
push ecx
mov eax, DWORD PTR _a$[ebp]
mov DWORD PTR tv64[ebp], eax
cmp DWORD PTR tv64[ebp], 0
je SHORT $LN4@f
cmp DWORD PTR tv64[ebp], 1
je SHORT $LN3@f
cmp DWORD PTR tv64[ebp], 2
je SHORT $LN2@f
jmp SHORT $LN1@f
$LN4@f:
push OFFSET $SG739 ; 'zero', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN3@f:
push OFFSET $SG741 ; 'one', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN2@f:
push OFFSET $SG743 ; 'two', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN7@f
$LN1@f:
push OFFSET $SG745 ; 'something unknown', 0aH, 00H
call _printf
add esp, 4
$LN7@f:
mov esp, ebp
pop ebp
ret 0
_f ENDP
上面这个函数的源程序相当于:
void f (int a)
{
if (a==0)
printf ("zero\n");
else if (a==1)
printf ("one\n");
else if (a==2)
printf ("two\n");
else
printf ("something unknown\n");
};
如果仅从汇编代码入手,那么我们无法判断上述函数是一个判断表达式较少的switch()语句、还是一组if()语句。确实可以认为,switch()语句是一种旨在简化大量嵌套if()语句而设计的语法糖[1]。
上面的汇编代码把输入参数a代入了临时的局部变量tv64,其余部分的指令都很好理解。[2]
若用GCC 4.4.1编译器编译这个程序,无论是否启用其最大程度优化的选项“-O3”,生成的汇编代码也和MSVC编译出来的代码没有什么区别。
Optimizing MSVC
经指令“c1 1.c /Fa1.asm /Ox”编译上述程序,可得到如下指令。
指令清单13.2 MSVC
_a$ = 8 ; size = 4
_f PROC
mov eax, DWORD PTR _a$[esp-4]
sub eax, 0
je SHORT $LN4@f
sub eax, 1
je SHORT $LN3@f
sub eax, 1
je SHORT $LN2@f
mov DWORD PTR _a$[esp-4], OFFSET $SG791 ; 'something unknown', 0aH, 00H
jmp _printf
$LN2@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG789 ; 'two', 0aH, 00H
jmp _printf
$LN3@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG787 ; 'one', 0aH, 00H
jmp _printf
$LN4@f:
mov DWORD PTR _a$[esp-4], OFFSET $SG785 ; 'zero', 0aH, 00H
jmp _printf
_f ENDP
我们看到,它有以下两处不同。
第一处:程序把变量a存储到EAX寄存器之后,又用EAX的值减去零。似乎这样做并没有什么道理。但是这两条指令可以检查EAX寄存器的值是否是零。如果EAX寄存器的值是零,ZF标志寄存器会被置1(也就是说0−0=0,这就可以提前设置ZF标志位),并会触发第一条条件转移指令JE,使程序跳转到 $LN4@f,继而在屏幕上打印“Zero”。如果EAX寄存器的值仍然不是零,则不会触发第一条跳转指令、做“EAX=EAX-1”的运算,若计算结果是零则做相应输出;若此时EAX寄存器的值仍然不是零,就会再做一次这种减法操作和条件判断。
如果三次运算都没能使EAX寄存器的值变为零,那么程序会输出最后一条信息“something unknown”。
第二处:在把字符串指针存储到变量a之后,函数使用JMP指令调用printf()函数。在调用printf()函数的时候,调用方函数而没有使用常规的call指令。这点不难解释:调用方函数把参数推送入栈之后,的确通常通过CALL指令调用其他函数。这种情况下,CALL指令会把返回地址推送入栈、并通过无条件转移的手段启用被调用方函数。就本例而言,在被调用方函数运行的任意时刻,栈的内存存储结构为:
ESP——指向RA。
ESP+4——指向变量a。
另一方面,在本例程序调用printf()函数之前、之后,除了制各第一个格式化字符串的参数问题以外,栈的存储结构其实没有发生变化。所以,编译器在分配JMP指令之前,把字符串指针存储到相应地址上。
这个程序把函数的第一个参数替换为字符串的指针,然后跳转到printf()函数的地址,就好像程序没有“调用”过f()函数、直接“转移”了printf()函数一般。当printf()函数完成输出的使命以后,它会执行RET返回指令。RET指令会从栈中读取(POP)返回地址RA、并跳转到RA。不过这个RA不是其调用方函数——f()函数内的某个地址,而是调用f()函数的函数即main()函数的某个地址。换而言之,跳转到这个RA地址后,printf()函数会伴随其调用方函数f()一同结束。
除非每个case从句的最后一条指令都是调用printf()函数,否则编译器就做不到这种程度的优化。某种意义上说这与longjmp()函数[3]十分相似。当然,这种优化的目的无非就是提高程序的运行速度。
ARM编译器也有类似的优化,请参见本书的6.2.1节。
OllyDbg
本节讲解使用OllyDbg调试这个程序的具体方法。
OllyDbg可以识别switch()语句的指令结构,而且能够自动地添加批注。最初的时候,EAX的值、即函数的输入参数为2,如图13.1所示。
图13.1 OllyDbg:观察EAX 里存储的函数的第一个(也是唯一一个)参数
EAX的值(2)减去0。当然,EAX里的值还是2。此后ZF标志位被设为0,代表着运算结果不是零,如图13.2所示。
图13.2 OllyDbg:执行第一个SUB指令
执行DEC指令之后,EAX里的值为1。但是1还不是零,ZF标志位还是0,如图13.3所示。
图13.3 OllyDbg:执行第一条DEC指令
再次执行DEC指令,此时EAX里的值终成为是零了。因为运算结果为零,ZF标志位被置位为1,如图13.4所示。
图13.4 OllyDbg:执行第二条DEC指令
OllyDbg提示将会触发条件转移指令,字符串“two”的指针即刻被推送入栈,如图13.5所示。
图13.5 OllyDbg:函数的第一个参数被赋值为字符串指针
请注意:函数的当前参数是2,它位于0x0020FA44处的栈。
MOV指令把地址0x0020FA44的指针放入栈中,然后进行跳转。程序将执行文件MSVCR100.DLL里的printf()函数的第一条指令。为了便于演示,本例在编译程序时使用了/MD开关,如图13.6所示。
图13.6 OllyDbg:文件MSVCR100.DLL中printf()函数的第一条指令
之后,printf()函数从地址0x00FF3010处读取它的唯一参数——字符串地址。然后函数会stdout设备(一般来说,就是屏幕)上在打印字符串。
printf()函数的最后一条指令如图13.7所示:
图13.7 OllyDbg:Printf()函数的最后一条指令
此时,字符串“two”就会被输出到控制台窗口(console)。
接下来,我们使用F7或F8键、单步执行这条返回指令。然而程序没有返回f()函数,而是回到了主函数main(),如图13.8所示。
图13.8 OllyDbg: 返回至main()函数
正如你所看到的那样,程序从printf()函数的内部直接返回到main()函数。这是因为RA寄存器里存储的返回地址确实不是f()函数中的某个地址,而是main()函数里的某个地址。请仔细观察返回地址的上一条指令,即“CALL 0X00FF1000”指令。它同样还是调用函数(即调用f())的指令。
.text:0000014C f1:
.text:0000014C 00 00 50 E3 CMP R0, #0
.text:00000150 13 0E 8F 02 ADREQ R0, aZero ; "zero\n"
.text:00000154 05 00 00 0A BEQ loc_170
.text:00000158 01 00 50 E3 CMP R0, #1
.text:0000015C 4B 0F 8F 02 ADREQ R0, aOne ; "one\n"
.text:00000160 02 00 00 0A BEQ loc_170
.text:00000164 02 00 50 E3 CMP R0, #2
.text:00000168 4A 0F 8F 12 ADRNE R0, aSomethingUnkno ; "something unknown\n"
.text:0000016C 4E 0F 8F 02 ADREQ R0, aTwo ; "two\n"
.text:00000170
.text:00000170 loc_170 ; CODE XREF: f1+8
.text:00000170 ; f1+14
.text:00000170 78 18 00 EA B __2printf
我们同样无法根据汇编指令判断源代码使用的是switch()语句还是if()语句。
此外,这段代码还出现了ADRQ指令之类的条件执行指令。第一条ADREQ指令会在R0=0的情况下,将字符串“zero \n”的地址传给R0。紧接其后都BEQ指令在相同的条件下把控制流转交给loc_170。或许有读者会问,ADREQ之后的BEQ指令还能读取到前面由CMP设置的标志吗?这当然不是问题。只有少数指令才会修改标志位寄存器的值,而一般的条件执行指令不会重设任何标志位。
其余的指令不难理解。程序只在尾部调用了一次printf()函数。前面的6.2.1节讲解过编译器的这种处理技术。最后,3条逻辑分支都会收敛于同一个printf()函数。
最后一条CMP指令是“CMP R0, #2”。它的作用是检查a是否为2。如果条件不成立,程序将通过ADRNE指令把“something unknown \n”的指针赋值给R0寄存器。在此之前,程序已经检查过a是否是0或1;所以运行到这里时,我们已经可以确定变量a不是这两个值。如果R0的值为2,那么ADREQ指令将把“two”的指针传递给R0寄存器。
.text:000000D4 f1:
.text:000000D4 10 B5 PUSH {R4,LR}
.text:000000D6 00 28 CMP R0, #0
.text:000000D8 05 D0 BEQ zero_case
.text:000000DA 01 28 CMP R0, #1
.text:000000DC 05 D0 BEQ one_case
.text:000000DE 02 28 CMP R0, #2
.text:000000E0 05 D0 BEQ two_case
.text:000000E2 91 A0 ADR R0, aSomethingUnkno ; "something unknown \n"
.text:000000E4 04 E0 B default_case
.text:000000E6 zero_case ; CODE XREF: f1+4
.text:000000E6 95 A0 ADR R0, aZero ; "zero\n"
.text:000000E8 02 E0 B default_case
.text:000000EA one_case ; CODE XREF: f1+8
.text:000000EA 96 A0 ADR R0, aOne ; "one\n"
.text:000000EC 00 E0 B default_case
.text:000000EE two_case ; CODE XREF: f1+C
.text:000000EE 97 A0 ADR R0, aTwo ; "two\n"
.text:000000F0 default_case ; CODE XREF: f1+10
.text:000000F0 ; f1+14
.text:000000F0 06 F0 7E F8 BL __2printf
.text:000000F4 10 BD POP {R4, PC}
前文介绍说过,条件执行指令是ARM模式指令的特色之一。Thumb模式的指令集里没有条件执行指令。所以Thumb模式的指令更为接近x86 CISC(复杂指令集)的指令,理解难度相对较低。
.LC12:
.string "zero"
.LC13:
.string "one"
.LC14:
.string "two"
.LC15:
.string "something unknown"
f12:
stp x29, x30, [sp, -32]!
add x29, sp, 0
str w0, [x29,28]
ldr w0, [x29,28]
cmp w0, 1
beq .L34
cmp w0, 2
beq .L35
cmp w0, wzr
bne .L38 ; jump to default label
adrp x0, .LC12 ; "zero"
add x0, x0, :lo12:.LC12
bl puts
b .L32
.L34:
adrp x0, .LC13 ; "one"
add x0, x0, :lo12:.LC13
bl puts
b .L32
.L35:
adrp x0, .LC14 ; "two"
add x0, x0, :lo12:.LC14
bl puts
b .L32
.L38:
adrp x0, .LC15 ; "something unknown"
add x0, x0, :lo12:.LC15
bl puts
nop
.L32:
ldp x29, x30, [sp], 32
ret
因为输入变量属于整型数据,所以ARM64编译器没有给它分配64位的X0寄存器而是为它分配了容量相当的X0寄存器。另外,本例用ADRP/ADD指令对向puts()函数传递字符串指针。前文3.4.5节中的“Hello,world!”也是这样传递参数的。
f12:
cmp w0, 1
beq .L31
cmp w0, 2
beq .L32
cbz w0, .L35
; default case
adrp x0, .LC15 ; "something unknown"
add x0, x0, :lo12:.LC15
b puts
.L35:
adrp x0, .LC12 ; "zero"
add x0, x0, :lo12:.LC12
b puts
.L32:
adrp x0, .LC14 ; "two"
add x0, x0, :lo12:.LC14
b puts
.L31:
adrp x0, .LC13 ; "one"
add x0, x0, :lo12:.LC13
b puts
优化编译的效果显著。CBZ(Compare and Branch on Zero)会在W0的值为零的情况下进行跳转。此外,在调用puts()函数的时候,本例使用的是JMP指令而非常规的call指令调用,这再现了13.1.1节出现过的函数调用方式。
指令清单13.3 Optimizing GCC 4.4.5(IDA)
f:
lui $gp, (__gnu_local_gp >> 16)
; is it 1?
Li $v0, 1
beq $a0, $v0, loc_60
la $gp, (__gnu_local_gp & 0xFFFF) ; branch delay slot
; is it 2?
Li $v0, 2
beq $a0, $v0, loc_4C
or $at, $zero ; branch delay slot, NOP
; jump, if not equal to 0:
bnez $a0, loc_38
or $at, $zero ; branch delay slot, NOP
; zero case:
lui $a0, ($LC0 >> 16) # "zero"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; load delay slot, NOP
jr $t9 ; branch delay slot, NOP
la $a0, ($LC0 & 0xFFFF) # "zero" ; branch delay slot
# ----------------------------------------------------------------
loc_38: # CODE XREF: f+1C
lui $a0, ($LC3 >> 16) # "something unknown"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; load delay slot, NOP
jr $t9
la $a0, ($LC3 & 0xFFFF) # "something unknown" ; branch delay slot
# ----------------------------------------------------------------
loc_4C: # CODE XREF: f+14
lui $a0, ($LC2 >> 16) # "two"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; load delay slot, NOP
jr $t9
la $a0, ($LC2 & 0xFFFF) # "two" ; branch delay slot
# ----------------------------------------------------------------
loc_60: # CODE XREF: f+8
lui $a0, ($LC1 >> 16) # "one"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; load delay slot, NOP
jr $t9
la $a0, ($LC1 & 0xFFFF) # "one" ; branch delay slot
在汇编层面,每个case分支的最后一条指令都是调用puts()函数的指令。而且本例的每个case分支都通过跳转指令JR(Jump Register)调用puts()函数,完全没有使用常规的函数调用指令JAL(Jump And Link)。有关详细介绍,请参阅13.1.1节。
另外,参数LW指令之后有一条NOP指令。这种指令组合叫作“加载延迟槽/load delay slot”,是MIPS平台的另一种延迟槽。在LW指令从内存加载数据的时候,下面的那条指令可能和它并发执行。这样一来,LW后面的那条指令就无法使用LW读取的数据了。当今主流的MIPS CPU都针对这一问题进行了优化,在下一条指令LW的数据的情况下能够进行自动处理。虽然现在的MIPS处理器不再存在加载延时槽,但是GCC还是会颇为保守地添加加载延迟槽。总之,我们已经可以忽视这种延迟槽了。
在case分支较少的情况下,switch()语句和if/else语句的编译结果基本相同。指令清单13.1可充分论证这个结论。
在switch()语句存在大量case()分支的情况下,编译器就不能直接套用大量JE/JNE指令了。否则程序代码肯定会非常庞大。
#include <stdio.h>
void f (int a)
{
switch (a)
{
case 0: printf ("zero\n"); break;
case 1: printf ("one\n"); break;
case 2: printf ("two\n"); break;
case 3: printf ("three\n"); break;
case 4: printf ("four\n"); break;
default: printf ("something unknown\n"); break;
};
};
int main()
{
f(2); // test
};
Non-optimizing MSVC
使用MSVC 2010编译上述程序,可得到如下指令。
指令清单13.4 MSVC 2010
tv64 = -4 ; size=4
_a$ = 8 ; size = 4
_f PROC
push ebp
mov ebp, esp
push ecx
mov eax, DWORD PTR _a$[ebp]
mov DWORD PTR tv64[ebp], eax
cmp DWORD PTR tv64[ebp], 4
ja SHORT $LN1@f
mov ecx, DWORD PTR tv64[ebp]
jmp DWORD PTR $LN11@f[ecx*4]
$LN6@f:
push OFFSET $SG739 ; 'zero', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN5@f:
push OFFSET $SG741 ; 'one', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN4@f:
push OFFSET $SG743 ; 'two', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN3@f:
push OFFSET $SG745 ; 'three', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN2@f:
push OFFSET $SG747 ; 'four', 0aH, 00H
call _printf
add esp, 4
jmp SHORT $LN9@f
$LN1@f:
push OFFSET $SG749 ; 'something unknown', 0aH, 00H
call _printf
add esp, 4
$LN9@f:
mov esp, ebp
pop ebp
ret 0
npad 2; align next label
$LN11@f:
DD $LN6@f ; 0
DD $LN5@f ; 1
DD $LN4@f ; 2
DD $LN3@f ; 3
DD $LN2@f ; 4
_f ENDP
这段代码可被分为数个调用printf()函数的指令组,而且每组指令传递给printf()函数的参数还各不相同。这些指令组在内存中拥有各自的起始地址,也就被编译器分配到了不同的符号标签(symbolic label)之后。总的来看,程序通过$LN11@f处的一组数据调派这些符号标签。
函数最初把变量a的值与数字4进行比较。如果a大于4,函数则跳转到$LN1@f处,把字符串“something unknown”的指针传递给printf()函数。
如果变量a小于或等于4,则会计算a乘以4的积,再计算积与$LN11@f的偏移量的和(表查询),并跳转到这个结果所指向的地址上。以变量a等于2的情况来说,2×4=8(由于x86系统的内存地址都是32位数据,所以$LN11@f表中的每个地址都占用4字节)。在计算8与$LN11@f的偏移量的和之后,再跳转到这个和指向的标签——即$LN4@f处。JMP指令最终跳转到$LN4@f的地址。
$LN11@f标签(偏移量)开始的表,叫作“转移表jumptable”,也叫作“转移(输出)表branchtable”。[4]
当a等于2的时候,程序分配给printf()的参数是“two”。实际上,此时的switch语句的分支指令等效于“jmp DWORD PTR $LN11@f[ecx*4]”。它会进行间接取值的操作,把指针“PTR{表达式}”所指向的数据读取出来,当作DWORD型数据传递给JMP指令。在这个程序里,表达式的值为$LN11@f+ecx*4。
此处出现的npad指令属于汇编宏,本书第88章会详细介绍它。它的作用是把紧接其后的标签地址向4字节(或16字节)边界对齐。npad的地址对齐功能可提高处理器的IO读写效率,通过一次操作即可完成内存总线、缓冲内存等设备的数据操作。
OllyDbg
接下来使用OllyDbg调试这个程序。我们在EAX=2的时候进行调试,如图13.9所示。
图13.9 使用OllyDbg查看EAX获取输入值的情况
程序将检验输入值是否大于4。因为2<4,所以不会执行“default”规则的跳转,如图13.10所示。
图13.10 OllyDbgEAX 4,不会触发default规则的跳转
然后就开始处理转移表,如图13.11所示。
图13.11 利用转移表计算目标地址
在用鼠标选择“Follow in Dump”→“Address constant”功能之后,即可在数据窗口看见转移表。表里有5个32位的值[5]。现在ECX寄存器的值是2,所以对应表中的第2个元素(从0开始数)。另外,您还可以使用OllyDbg的“Follow in Dump→Memory address”功能查看JMP指令的目标地址。此时,这个目标地址为0x010B103A。
地址0x010B103A处的指令将会打印字符串“two”,如图13.12所示。
图13.12 使用OllyDbg观察case:label的触发过程
Non-optimizing GCC
GCC 4.4.1编译出的代码如下。
指令清单13.5 GCC 4.4.1
public f
f proc near ; CODE XREF: main+10
var_18 = dword ptr -18h
arg_0 = dword ptr 8
push ebp
mov ebp, esp
sub esp, 18h
cmp [ebp+arg_0], 4
ja short loc_8048444
mov eax, [ebp+arg_0]
shl eax, 2
mov eax, ds:off_804855C[eax]
jmp eax
loc_80483FE: ; DATA XREF: .rodata:off_804855C
mov [esp+18h+var_18], offset aZero ; "zero"
call _puts
jmp short locret_8048450
loc_804840C: ; DATA XREF: .rodata:08048560
mov [esp+18h+var_18], offset aOne ; "one"
call _puts
jmp short locret_8048450
loc_804841A: ; DATA XREF: .rodata:08048564
mov [esp+18h+var_18], offset aTwo ; "two"
call _puts
jmp short locret_8048450
loc_8048428: ; DATA XREF: .rodata:08048568
mov [esp+18h+var_18], offset aThree ; "three"
call _puts
jmp short locret_8048450
loc_8048436: ; DATA XREF: .rodata:0804856C
mov [esp+18h+var_18], offset aFour ; "four"
call _puts
jmp short locret_8048450
loc_8048444: ; CODE XREF: f+A
mov [esp+18h+var_18], offset aSomethingUnkno ; "something unknown"
call _puts
locret_8048450: ; CODE XREF: f+26
; f+34...
leave
retn
f endp
off_804855C dd offset loc_80483FE ; DATA XREF: f+12
dd offset loc_804840C
dd offset loc_804841A
dd offset loc_8048428
dd offset loc_8048436
这段代码与MSVC编译出来的代码几乎相同。参数arg_0被左移2位(数学上等同于乘以4,有关指令介绍请参阅16.2.1节),然后在转移表off_804855C的数组中获取相应地址,并将计算结果存储于EAX寄存器。最后通过JMP EAX指令进行跳转。
指令清单13.6 Optimizing Keil 6/2013 (ARM mode)
00000174 f2
00000174 05 00 50 E3 CMP R0, #5 ; switch 5 cases
00000178 00 F1 8F 30 ADDCC PC, PC, R0,LSL#2 ; switch jump
0000017C 0E 00 00 EA B default_case ; jumptable 00000178 default case
00000180
00000180 loc_180 ; CODE XREF: f2+4
00000180 03 00 00 EA B zero_case ; jumptable 00000178 case 0
00000184
00000184 loc_184 ; CODE XREF: f2+4
00000184 04 00 00 EA B one_case ; jumptable 00000178 case 1
00000188
00000188 loc_188 ; CODE XREF: f2+4
00000188 05 00 00 EA B two_case ; jumptable 00000178 case 2
0000018C
0000018C loc_18C ; CODE XREF: f2+4
0000018C 06 00 00 EA B three_case ; jumptable 00000178 case 3
00000190
00000190 loc_190 ; CODE XREF: f2+4
00000190 07 00 00 EA B four_case ; jumptable 00000178 case 4
00000194
00000194 zero_case ; CODE XREF: f2+4
00000194 ; f2:loc_180
00000194 EC 00 8f E2 ADR R0, aZero ; jumptable 00000178 case 0
00000198 04 00 00 EA B loc_1B8
0000019C
0000019C one_case ; CODE XREF: f2+4
0000019C ; f2:loc_184
0000019C EC 00 8F E2 ADR R0, aOne ; jumptable 00000178 case 1
000001A0 04 00 00 EA B loc_1B8
000001A4
000001A4 two_case ; CODE XREF: f2+4
000001A4 ; f2:loc_188
000001A4 01 0C 8F E2 ADR R0, aTwo ; jumptable 00000178 case 2
000001A8 02 00 00 EA B loc_1B8
000001AC
000001AC three_case ; CODE XREF: f2+4
000001AC ; f2:loc_18C
000001AC 01 0C 8F E2 ADR R0, aThree ; jumptable 00000178 case 3
000001B0 00 00 00 EA B loc_1B8
000001B4
000001B4 four_case ; CODE XREF: f2+4
000001B4 ; f2:loc_190
000001B4 01 0C 8F E2 ADR R0, aFour ; jumptable 00000178 case 4
000001B8
000001B8 loc_1B8 ; CODE XREF: f2+4
000001B8 ; f2+2C
000001B8 66 18 10 EA B __2printf
000001BC default_case ; CODE XREF: f2+4
000001BC ; f2+8
000001BC D4 00 8F E2 ADR R0, aSomethingUnkno ; jumptable default case
000001C0 FC FF FF EA B loc_1B8
这段代码充分体现了ARM模式下每条汇编指令占用4个字节的特点。
这个程序能够识别出4及4以下的自然数。当输入值是大于4的整数时,程序都会显示“something unknown \n”。
第一条指令是“CMP R0, #5”。它将输入变量与5做比较。
“ADDCC PC, PC, R0, LSL#2”会在R0寄存器的值小于5的时候进行加法计算,其中CC代表借位标志Carry Clear。如果R0寄存器的值不小于5,(即R0大于或等于5),则会直接跳转到标签default_case 处。
如果R0寄存器的值是5以下的整数,那么将会触发ADDCC,并且进行下列运算:
将R0的值乘以4。LSL是左移操作,左移两位(2bits)就相当于乘以4。
把上述积与PC的值相加,并会把运算结果存储在PC寄存器里。这种调整PC指针的操作,等同于运行B跳转指令。
在执行ADDCC指令的时候,PC寄存器的值会比当前指令的(首)地址提前8个字节。此时ADDCC的地址是0x178,PC的值为0x180。即,PC指向当前指令后面的第二条指令。
这是ARM处理器的pipeline/流水线决定的。当ARM处理器执行某条指令时,处理器正在处理(fetch取指)后面的第二条指令。实际上PC指向后面第二条(正在被fetch/取指的)指令的地址。[6]
如果a==0,“加零”操作使PC寄存器的值不变。所以在PC操作之后,CPU会跳到8个字节之后的loc_180处继续执行后续命令,开始执行ADDCC指令。
如果a==1,则PC+8+a×4=PC+16=0x184。程序会跳转到loc_184处。
依此类推,变量a的值每增加1,PC的值就会增加4。这4字节是每个分支语句的唯一一条指令——B指令的opcode的长度。在ADDCC之后,总共有5个B跳转指令。
后面的指令比较容易理解,5条B跳转指令接着完成各自赋值和打印的任务,完成switch()语句的功能。
指令清单13.7 Optimizing Keil 6/2013 (Thumb mode)
000000F6 EXPORT f2
000000F6 f2
000000F6 10 B5 PUSH {R4,LR}
000000F8 03 00 MOVS R3, R0
000000FA 06 F0 69 F8 BL __ARM_common_switch8_thumb ; switch 6 cases
000000FE 05 DCB 5
000000FF 04 06 08 0A 0C 10 DCB 4, 6, 8, 0xA, 0xC, 0x10 ; jump table for switch statement
00000105 00 ALIGN 2
00000106
00000106 zero_case ; CODE XREF: f2+4
00000106 8D A0 ADR R0, aZero ; jump table 000000FA case 0
00000108 06 E0 B loc_118
0000010A
0000010A one_case ; CODE XREF: f2+4
0000010A 8E A0 ADR R0, aOne ; jumptable 000000FA case 1
0000010C 04 E0 B loc_118
0000010E
0000010E two_case ; CODE XREF: f2+4
0000010E 8F A0 ADR R0, aTwo ; jumptable 000000FA case 2
00000110 02 E0 B loc_118
00000112
00000112 three_case ; CODE XREF: f2+4
00000112 90 A0 ADR R0, aThree ; jumptable 000000FA case 3
00000114 00 E0 B loc_118
00000116
00000116 four_case ; CODE XREF: f2+4
00000116 91 A0 ADR R0, aFour ; jumptable 000000FA case 4
00000118
00000118 loc_118 ; CODE XREF: f2+12
00000118 ; f2+16
00000118 06 F0 6A F8 BL __2printf
0000011C 10 BD POP {R4,PC}
0000011E
0000011E default_case ; CODE XREF: f2+4
0000011E 82 A0 ADR R0, aSomethingUnkno ; jumptable 000000FA default case
00000120 FA E7 B loc_118
000061D0 EXPORT __ARM_common_switch8_thumb
000061D0 __ARM_common_switch8_thumb ; CODE XREF: example6_f2+4
000061D0 78 47 BX PC
000061D2 00 00 ALIGN 4
000061D2 ; End of function __ARM_common_switch8_thumb
000061D2
000061D4 __32__ARM_common_switch8_thumb ; CODE XREF__ARM_common_switch8_thumb
000061D4 01 C0 5E E5 LDRB R12, [LR,#-1]
000061D8 0C 00 53 E1 CMP R3, R12
000061DC 0C 30 DE 27 LDRCSB R3, [LR,R12]
000061E0 03 30 DE 37 LDRCCB R3, [LR,R3]
000061E4 83 C0 8E E0 ADD R12, LR, R3,LSL#1
000061E8 1C FF 2F E1 BX R12
000061E8 ; End of function __32__ARM_common_switch8_thumb
Thumb和Thumb-2程序的opcode长度并不固定。这一特征更接近x86系统的程序。
它们的程序代码里有一个专门用于存储case从句信息(default以外)的表。这个表负责记录case从句的数量、偏移量和标签,以便程序可以进行准确的寻址。程序通过这个表单进行相应的跳转,继而处理相应的分支case语句。
因为需要操作转移表并进行后续跳转,所以这个程序也使用了专用函数_ARM_common_ switch8_thumb。这个函数的第一条指令是“BX PC”,它将运行模式切换到32位的ARM模式,然后在32位模式下进行操作。然后函数着手表查询和分支转移单操作。具体指令非常复杂,本文在这里只是简单介绍一下,暂时不进行详解。
比较有趣的是,这个函数使用LR寄存器存储表的指针。在调用这个函数之后,LR寄存器存储着“BL __ARM_common_switch8_thumb”的后续指令的地址,也就是表开始的地址。
这个程序出现了每个switch()陈述句都会复用的专用函数,具有显著的结构化特征。可能是编译器为了避免生成重复代码而进行的处理。
IDA能够自动识别出这个函数和相应的转移表。IDA还能给相应的条目加上合理的注释。举例来说,IDA就给本例添加了“jumptable 000000FA case 0”这样的注释。
指令清单13.8 Optimizing GCC 4.4.5 (IDA)
f:
lui $gp, (__gnu_local_gp >> 16)
; jump to loc_24 if input value is lesser than 5:
sltiu $v0, $a0, 5
bnez $v0, loc_24
la $gp, (__gnu_local_gp & 0xFFFF) ; branch delay slot
; input value is greater or equal to 5.
; print "something unknown" and finish:
lui $a0, ($LC5 >> 16) # "something unknown"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC5 & 0xFFFF) # "something unknown" ; branch delay slot
loc_24: # CODE XREF: f+8
; load address of jumptable
; LA is pseudoinstruction, LUI and ADDIU pair are there in fact:
la $v0, off_120
; multiply input value by 4:
sll $a0, 2
; sum up multiplied value and jumptable address:
addu $a0, $v0, $a0
; load element from jumptable:
lw $v0, 0($a0)
or $at, $zero ; NOP
; jump to the address we got in jumptable:
jr $v0
or $at, $zero ; branch delay slot, NOP
sub_44: # DATA XREF: .rodata:0000012C
; print "three" and finish
lui $a0, ($LC3 >> 16) # "three"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC3 & 0xFFFF) # "three" ; branch delay slot
sub_58: # DATA XREF: .rodata:00000130
; print "four" and finish
lui $a0, ($LC4 >> 16) # "four"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC4 & 0xFFFF) # "four" ; branch delay slot
sub_6C: # DATA XREF: .rodata:off_120
; print "zero" and finish
lui $a0, ($LC0 >> 16) # "zero"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC0 & 0xFFFF) # "zero" ; branch delay slot
sub_80: # DATA XREF: .rodata:00000124
; print "one" and finish
lui $a0, ($LC1 >> 16) # "one"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC1 & 0xFFFF) # "one" ; branch delay slot
sub_94: # DATA XREF: .rodata:00000128
; print "two" and finish
lui $a0, ($LC2 >> 16) # "two"
lw $t9, (puts & 0xFFFF)($gp)
or $at, $zero ; NOP
jr $t9
la $a0, ($LC2 & 0xFFFF) # "two" ; branch delay slot
; may be placed in .rodata section:
off_120: .word sub_6C
.word sub_80
.word sub_94
.word sub_44
.word sub_58
上述代码出现了SLTIU(Set on Less Than lmmediate Unsigned)指令。它和SLTU(Set on Less Than Unsigned)的功能基本相同。请注意,这两个指令名称里差了一个“立即数(immediate)”字样。这说明前者需要在指令中指定既定的立即数。
BNEZ是“在非零情况下进行转移/Branche if Not Equal to Zero”的缩写。
上述代码和其他指令集的代码十分相近。SLL(Shift Word Left Logical)是逻辑左移的指令,本例用它进行“乘以4”的运算。毕竟这是一个面向32位MIPS CPU的程序,所有转移表里的所有地址都是32位指针。
switch()的大体框架参见指令清单13.9。
指令清单13.9 x86
MOV REG,input
CMP REG,4 ; maximal number of cases
JA default
SHL REG,3 ; find element in table.shift for 3bits in x64.
MOV REG, jump_table[REG]
JMP REG
case1;
; do something
JMP exit
case2;
; do something
JMP exit
case3;
; do something
JMP exit
case4;
; do something
JMP exit
Case5;
; do something
JMP exit
defaule:
…
exit:
…
jump_table dd casel
dd case2
dd case3
dd case4
dd case5
若不使用上述指令,我们也可以在32位系统上使用指令JMP jump_table[REG*4]/在64位上使用JMP jump_table[REG*8],实现转移表中的寻址计算。
说到底,转移表只不过是某种指针数组它和18.5节介绍的那种指针数组十分雷同。
多个case陈述从句触发同一系列操作的情况并不少见,例如:
#include <stdio.h>
void f (int a)
{
switch (a)
{
case 1:
case 2:
case 7:
case 10:
printf ("1, 2, 7, 10\n");
break;
case 3:
case 4:
case 5:
case 6:
printf ("3, 4, 5\n");
break;
case 8:
case 9:
case 20:
case 21:
printf ("8 9, 21\n");
break;
case 22:
printf ("22\n");
break;
default:
printf ("default\n");
break;
};
};
int main ()
{
f(4);
};
如果编译器刻板地按照每种可能的逻辑分支逐一分配对应的指令组,那么程序里将会存在大量的重复指令。一般而言,编译器会通过某种派发机制来降低代码的冗余度。
使用MSVC 2010(启用/Ox选项)编译上述程序,可得到如下指令。
指令清单13.10 Optimizing MSVC 2010
1 $SG2798 DB '1, 2, 7, 10', 0aH, 00H
2 $SG2800 DB '3, 4, 5', 0aH, 00H
3 $SG2802 DB '8, 9, 21', 0aH, 00H
4 $SG2804 DB '22', 0aH, 00H
5 $SG2806 DB 'default', 0aH, 00H
6
7 _a$ = 8
8 _f PROC
9 mov eax, DWORD PTR _a$[esp-4]
10 dec eax
11 cmp eax, 21
12 ja SHORT $LN1@f
13 movzx eax, BYTE PTR $LN10@f[eax]
14 jmp DWORD PTR $LN11@f[eax*4]
15 $LN5@f:
16 mov DWORD PTR _a$[esp-4], OFFSET $SG2798 ; '1, 2, 7, 10'
17 jmp DWORD PTR __imp__printf
18 $LN4@f:
19 mov DWORD PTR _a$[esp-4], OFFSET $SG2800 ; '3, 4, 5'
20 jmp DWORD PTR __imp__printf
21 $LN3@f:
22 mov DWORD PTR _a$[esp-4], OFFSET $SG2802 ; '8, 9, 21'
23 jmp DWORD PTR __imp__printf
24 $LN2@f:
25 mov DWORD PTR _a$[esp-4], OFFSET $SG2804 ; '22'
26 jmp DWORD PTR __imp__printf
27 $LN1@f:
28 mov DWORD PTR _a$[esp-4], OFFSET $SG2806 ; 'default'
29 jmp DWORD PTR __imp__printf
30 npad 2 ; align $LN11@f table on 16-byte boundary
31 $LN11@f:
32 DD $LN5@f ; print '1, 2, 7, 10'
33 DD $LN4@f ; print '3, 4, 5'
34 DD $LN3@f ; print '8, 9, 21'
35 DD $LN2@f ; print '22'
36 DD $LN1@f ; print 'default'
37 $LN10@f:
38 DB 0 ; a=1
39 DB 0 ; a=2
40 DB 1 ; a=3
41 DB 1 ; a=4
42 DB 1 ; a=5
43 DB 1 ; a=6
44 DB 0 ; a=7
45 DB 2 ; a=8
46 DB 2 ; a=9
47 DB 0 ; a=10
48 DB 4 ; a=11
49 DB 4 ; a=12
50 DB 4 ; a=13
51 DB 4 ; a=14
52 DB 4 ; a=15
53 DB 4 ; a=16
54 DB 4 ; a=17
55 DB 4 ; a=18
56 DB 4 ; a=19
57 DB 2 ; a=20
58 DB 2 ; a=21
59 DB 3 ; a=22
60 _f ENDP
这个程序用到了两个表:一个是索引表$LN10@f;另一个是转移表$LN11@f。
第13行的movzx指令在索引表里查询输入值。
索引表的返回值又分为0(输入值为1、2、7、10)、1(输入值为3、4、5)、2(输入值为8、9、21)、3(输入值为22)、4(其他值)这5种情况。
程序把索引表的返回值作为关键字,再在第二个转移表里进行查询,以完成相应跳转(第14行指令的作用)。
需要注意的是,输入值为0的情况没有相应的case从句。如果a=0,则“dec eax”指令会继续进行计算,而$LN10@f表的查询是从1开始的。可见,没有必要为0的特例设置单独的表。
这是一种普遍应用的编译技术。
表面看来,这种双表结构似乎不占优势。为什么它不象前文(请参见13.2.1节)那样采用一个统一的指针结构呢?在这种双表结构中,索引表采用的是byte型数据,所以双表结构比前面那种单表结构更为紧凑。
在编译这种多对一的switch语句时,GCC会生成统一的转移表。其代码风格和前文13.2.1节的风格相同。
因为输入值为零的情况没有对应的处理方法,所以GCC会从输入值为1的特例开始枚举各个分支,以便把转移表压缩得尽可能小。
GCC 4.9.1for ARM64的编译技术更为优越。它能把所有的偏移量信息编码为8位字节型数据、封装在单条指令的opcode里。前文介绍过,ARM64程序的每条指令都对应着4个字节的opcode。在本例这种类型的小型代码中,各分支偏移量的具体数值不会很大。GCC能够充分利用这一现象,构造出单字节指针组成的转移表。
指令清单13.11 Optimizing GCC 4.9.1 ARM64
f14:
; input value in W0
sub w0, w0, #1
cmp w0, 21
; branch if less or equal (unsigned):
bls .L9
.L2:
; print "default":
adrp x0, .LC4
add x0, x0, :lo12:.LC4
b puts
.L9:
; load jumptable address to X1:
adrp x1, .L4
add x1, x1, :lo12:.L4
; W0=input_value-1
; load byte from the table:
ldrb w0, [x1,w0,uxtw]
; load address of the Lrtx label:
adr x1, .Lrtx4
; multiply table element by 4 (by shifting 2 bits left) and add (or subtract) to the address of lrtx
add x0, x1, w0, sxtb #2
; jump to the calculated address:
br x0
; this label is pointing in code (text) segment:
.Lrtx4:
.section .rodata
; everything after ".section" statement is allocated in the read-only data (rodata) segment:
.L4:
.byte (.L3 - .Lrtx4) / 4 ;case 1
.byte (.L3 - .Lrtx4) / 4 ;case 2
.byte (.L5 - .Lrtx4) / 4 ;case 3
.byte (.L5 - .Lrtx4) / 4 ;case 4
.byte (.L5 - .Lrtx4) / 4 ;case 5
.byte (.L5 - .Lrtx4) / 4 ;case 6
.byte (.L3 - .Lrtx4) / 4 ;case 7
.byte (.L6 - .Lrtx4) / 4 ;case 8
.byte (.L6 - .Lrtx4) / 4 ;case 9
.byte (.L3 - .Lrtx4) / 4 ;case 10
.byte (.L2 - .Lrtx4) / 4 ;case 11
.byte (.L2 - .Lrtx4) / 4 ;case 12
.byte (.L2 - .Lrtx4) / 4 ;case 13
.byte (.L2 - .Lrtx4) / 4 ;case 14
.byte (.L2 - .Lrtx4) / 4 ;case 15
.byte (.L2 - .Lrtx4) / 4 ;case 16
.byte (.L2 - .Lrtx4) / 4 ;case 17
.byte (.L2 - .Lrtx4) / 4 ;case 18
.byte (.L2 - .Lrtx4) / 4 ;case 19
.byte (.L6 - .Lrtx4) / 4 ;case 20
.byte (.L6 - .Lrtx4) / 4 ;case 21
.byte (.L7 - .Lrtx4) / 4 ;case 22
.text
; everything after ".text" statement is allocated in the code (text) segment:
.L7:
; print "22"
adrp x0, .LC3
add x0, x0, :lo12:.LC3
b puts
.L6:
; print "8, 9, 21"
adrp x0, .LC2
add x0, x0, :lo12:.LC2
b puts
.L5:
; print "3, 4, 5"
adrp x0, .LC1
add x0, x0, :lo12:.LC1
b puts
.L3:
; print "1, 2, 7, 10"
adrp x0, .LC0
add x0, x0, :lo12:.LC0
b puts
.LC0:
.string "1, 2, 7, 10"
.LC1:
.string "3, 4, 5"
.LC2:
.string "8, 9, 21"
.LC3:
.string "22"
.LC4:
.string "default"
把上述程序编译为obj文件,然后再使用IDA打开,可看到其转移表如下。
指令清单13.12 jumptable in IDA
.rodata:0000000000000064 AREA .rodata, DATA, READONLY
.rodata:0000000000000064 ; ORG 0x64
.rodata:0000000000000064 $d DCB 9 ; case 1
.rodata:0000000000000065 DCB 9 ; case 2
.rodata:0000000000000066 DCB 6 ; case 3
.rodata:0000000000000067 DCB 6 ; case 4
.rodata:0000000000000068 DCB 6 ; case 5
.rodata:0000000000000069 DCB 6 ; case 6
.rodata:000000000000006A DCB 9 ; case 7
.rodata:000000000000006B DCB 3 ; case 8
.rodata:000000000000006C DCB 3 ; case 9
.rodata:000000000000006D DCB 9 ; case 10
.rodata:000000000000006E DCB 0xF7 ; case 11
.rodata:000000000000006F DCB 0xF7 ; case 12
.rodata:0000000000000070 DCB 0xF7 ; case 13
.rodata:0000000000000071 DCB 0xF7 ; case 14
.rodata:0000000000000072 DCB 0xF7 ; case 15
.rodata:0000000000000073 DCB 0xF7 ; case 16
.rodata:0000000000000074 DCB 0xF7 ; case 17
.rodata:0000000000000075 DCB 0xF7 ; case 18
.rodata:0000000000000076 DCB 0xF7 ; case 19
.rodata:0000000000000077 DCB 3 ; case 20
.rodata:0000000000000078 DCB 3 ; case 21
.rodata:0000000000000079 DCB 0 ; case 22
.rodata:000000000000007B ; .rodata ends
当输入值为1时,目标偏移量的技术方法是:9乘以4、再加上Lrtx4的偏移量。当输入值为22时,目标偏移量为:0乘以4、结果为0。在转移表Lrtx4之后就是L7的标签的指令了,这部分指令将负责打印数字22。请注意,转移表位于单独的.rodata段。编译器没有把它分配到.text的代码段里。
上述转移表有一个负数0xF7。这个偏移量指向了打印默认字符串(.L2标签)的相关指令。
Switch()语句还有一种常见的使用方法——fall-through。
1 #define R 1
2 #define W 2
3 #define RW 3
4
5 void f(int type)
6 {
7 int read=0, write=0;
8
9 switch (type)
10 {
11 case RW:
12 read=1;
13 case W:
14 write=1;
15 break;
16 case R:
17 read=1;
18 break;
19 default:
20 break;
21 };
22 printf ("read=%d, write=%d\n", read, write);
23 };
如果type为1(参见第一行可知,这是读取权限R为真的情况),则read的值会被设置为1;如果type为2(W),则write被设置1;如果type为3(RW),则read和write的值都会被设置为1。
无论type的值是RW还是W,程序都会执行第14行的指令。type为RW的陈述语句里没有break指令,从而利用switch语句的fall through效应。
指令清单13.13 MSVC 2012
$SG1305 DB 'read=%d, write=%d', 0aH, 00H
_write$ = -12 ; size= 4
_read$ = -8 ; size= 4
tv64 = -4 ; size= 4
_type$ = 8 ; size= 4
_f PROC
push ebp
mov ebp, esp
sub esp, 12
mov DWORD PTR _read$[ebp], 0
mov DWORD PTR _write$[ebp], 0
mov eax, DWORD PTR _type$[ebp]
mov DWORD PTR tv64[ebp], eax
cmp DWORD PTR tv64[ebp], 1 ; R
je SHORT $LN2@f
cmp DWORD PTR tv64[ebp], 2 ; W
je SHORT $LN3@f
cmp DWORD PTR tv64[ebp], 3 ; RW
je SHORT $LN4@f
jmp SHORT $LN5@f
$LN4@f: ; case RW:
mov DWORD PTR _read$[ebp], 1
$LN3@f: ; case W:
mov DWORD PTR _write$[ebp], 1
jmp SHORT $LN5@f
$LN2@f: ; case R:
mov DWORD PTR _read$[ebp], 1
$LN5@f: ; default
mov ecx, DWORD PTR _write$[ebp]
push ecx
mov edx, DWORD PTR _read$[ebp]
push edx
push OFFSET $SG1305 ; 'read=%d, write=%d'
call _printf
add esp, 12
mov esp, ebp
pop ebp
ret 0
_f ENDP
上述汇编指令与C语言源代码几乎一一对应。因为在$LN4@f和$LN3@f之间没有转移指令,所以当程序执行了$LN4@f处的“令read的值为1”的指令之后,它还会执行后面那个标签的write赋值指令。这也是“fall through”(滑梯)这个名字的来源:当执行完前面那个陈述语句的指令(read赋值)之后,继续执行下一个陈述语句的指令(write赋值)。如果type的值为W,程序会执行$LN3@f的指令,而不会执行前面那个read赋值指令。
指令清单13.14 GCC (Linaro) 4.9
.LC0:
.string "read=%d, write=%d\n"
f:
stp x29, x30, [sp, -48]!
add x29, sp, 0
str w0, [x29,28]
str wzr, [x29,44] ; set "read" and "write" local variables to zero
str wzr, [x29,40]
ldr w0, [x29,28] ; load "type" argument
cmp w0, 2 ; type=W?
beq .L3
cmp w0, 3 ; type=RW?
beq .L4
cmp w0, 1 ; type=R?
beq .L5
b .L6 ; otherwise...
.L4: ; case RW
mov w0, 1
str w0, [x29,44] ; read=1
.L3: ; case W
mov w0, 1
str w0, [x29,40] ; write=1
b .L6
.L5: ; case R
mov w0, 1
str w0, [x29,44] ; read=1
nop
.L6: ; default
adrp x0, .LC0 ; "read=%d, write=%d\n"
add x0, x0, :lo12:.LC0
ldr w1, [x29,44] ; load "read"
ldr w2, [x29,40] ; load "write"
bl printf
ldp x29, x30, [sp], 48
ret
Arm64程序的汇编指令与MSVC x86的汇编指令大致相同。编译器同样没有在标签.L4和标签.L3之间分配转移指令,从而形成了Switch()语句的fall-through效应。
13.2节有一段C语言源代码。请改写这个程序,并且在不改变程序功能的前提下,让编译器生成体积更小的可执行程序。
[1] 即syntactic sugar,指代增强代码可读性、降低编程出错率的语法改进措施。
[2] MSVC编译器在处理栈内的局部变量时,按照其需要,可能给这些内部变量分配以tv开头的宏变量。
[3] https://en.wikipedia.org/wiki/Setjmp.h。
[4] 这个名称来自于Fortran早期的GOTO算法。虽然现在保留了这个名称,但是已经和那个概念没什么关系了。详情请参见http://en.wikipedia.org/wiki/Branch_table。
[5] OllyDbg用下画线的格式显示这些值,因为它们也是FIXUPS。本书的68.2.6节会进行详细的解释。
[6] 虽然pipeline三级流水的解释较为直观,但是官方手册《ARM architecture reference manual》第1章第2节否认了这种硬件上的联系,它把PC与指令间opcode的增量关系解释为历史原因。