功能结语之前的多余 JMP
底部的JMP转到下一条语句,这已在 commit 中修复。TCC 0.9.27 版解决了这个问题:
当 'return' 是顶层块的最后一条语句(非常常见且经常推荐的情况)时,不需要跳转。
至于它最初存在的原因?这个想法是每个函数都有一个可能的共同退出点。如果在底部有一个带有返回的代码块,则JMP会转到一个公共退出点,在此完成堆栈清理并ret
执行 。最初,如果 JMP 指令出现在最后(右大括号)之前,代码生成器也会在函数末尾错误地发出JMP指令。该修复程序检查函数顶层}
是否有后跟右大括号的语句。return
如果有,则省略JMP
一个在右大括号之前在较低范围内返回的代码示例:
int main(int argc, char *argv[])
{
if (argc == 3) {
argc++;
return argc;
}
argc += 3;
return argc;
}
生成的代码如下所示:
401000: 55 push ebp
401001: 89 e5 mov ebp,esp
401003: 81 ec 00 00 00 00 sub esp,0x0
401009: 90 nop
40100a: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
40100d: 83 f8 03 cmp eax,0x3
401010: 0f 85 11 00 00 00 jne 0x401027
401016: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
401019: 89 c1 mov ecx,eax
40101b: 40 inc eax
40101c: 89 45 08 mov DWORD PTR [ebp+0x8],eax
40101f: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
; Jump to common function exit point. This is the `return argc` inside the if statement
401022: e9 11 00 00 00 jmp 0x401038
401027: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
40102a: 83 c0 03 add eax,0x3
40102d: 89 45 08 mov DWORD PTR [ebp+0x8],eax
401030: 8b 45 08 mov eax,DWORD PTR [ebp+0x8]
; Jump to common function exit point. This is the `return argc` at end of the function
401033: e9 00 00 00 00 jmp 0x401038
; Common function exit point
401038: c9 leave
401039: c3 ret
在0.9.27之前return argc
的版本中,if 语句内部会跳转到一个共同的退出点(函数结尾处)。同样return argc
,函数底部的 也跳转到函数的同一个公共退出点。问题是函数的公共退出点恰好在顶层之后,return argc
因此副作用是一个额外的 JMP,恰好是下一条指令。
函数序言后的 NOP
NOP不是为了对齐。由于 Windows为堆栈(可移植可执行格式的程序)实现保护页的方式,TCC 有两种类型的序言。如果需要的本地堆栈空间 < 4096(小于单个页面),那么您会看到生成了这种代码:
401000: 55 push ebp
401001: 89 e5 mov ebp,esp
401003: 81 ec 00 00 00 00 sub esp,0x0
sub esp,0
没有优化出来。它是局部变量所需的堆栈空间量(在本例中为 0)。如果添加一些局部变量,您将看到SUB指令中的 0x0 更改为与局部变量所需的堆栈空间量一致。这个序言需要 9 个字节。还有另一个序言来处理所需的堆栈空间 >= 4096 字节的情况。如果您添加一个 4096 字节的数组,例如:
char somearray[4096]
并查看生成的指令,您将看到函数序言变为 10 字节序言:
401000: b8 00 10 00 00 mov eax,0x1000
401005: e8 d6 00 00 00 call 0x4010e0
当面向 WinPE 时,TCC 的代码生成器假定函数序言始终为 10 个字节。这主要是因为 TCC 是单通道编译器。在处理函数之前,编译器不知道函数将使用多少堆栈空间。为了避免提前知道这一点,TCC 为序言预先分配了 10 个字节以适应最大的方法。任何更短的都填充到 10 个字节。
在需要堆栈空间 < 4096 字节的情况下,指令总共使用了 9 个字节。NOP用于将序言填充到 10 个字节。对于需要 >= 4096 字节的情况,在EAX中传递字节数并调用函数__chkstk
来分配所需的堆栈空间。