在编译阶段,将会被编译器把函数体展开并嵌入到每一个调用点的函数,就是内联函数。
指令清单43.1 一个简单的例子
#include <stdio.h>
int celsius_to_fahrenheit (int celsius)
{
return celsius * 9 / 5 + 32;
};
int main(int argc, char *argv[])
{
int celsius=atol(argv[1]);
printf ("%d\n", celsius_to_fahrenheit (celsius));
};
此函数在汇编层面的具体指令与源代码几乎一一对应。然而,如果在GCC编译环境下,我们采用-03参数优化的话,我们会看到如下所示的代码。
指令清单43.2 GCC 4.8.1优化
_main:
push ebp
mov ebp, esp
and esp, -16
sub esp, 16
call ___main
mov eax, DWORD PTR [ebp+12]
mov eax, DWORD PTR [eax+4]
mov DWORD PTR [esp], eax
call _atol
mov edx, 1717986919
mov DWORD PTR [esp], OFFSET FLAT:LC2 ; "%d\12\0"
lea ecx, [eax+eax*8]
mov eax, ecx
imul edx
sar ecx, 31
sar edx
sub edx, ecx
add edx, 32
mov DWORD PTR [esp+4], edx
call _printf
leave
ret
值得注意的是,编译器用乘法指令变相地实现了除法运算(可以参见第41章)。
您没看错,温度转换函数celsius_to_fahrenheit()(摄氏温度转换成华氏温度)的函数体被直接展开,放在了函数printf()(显示字符串)的前面。为什么呢?因为这样运行速度会更快一些。这种代码不需要在调用函数时处理多余的函数调用和返回指令。
现在具有优化功能的编译器一般都能自动的把小型函数的函数体直接“嵌入”到调用方函数的代码里。当然我们也可以借助关键字“inline”强制编译器进行这种“嵌入”处理。
在处理字符串和内存操作的常见函数时,例如:strycpy()、strcmp()、strlen、memset()、memcpy()、memcmp()函数,编译器通常会把这些函数当作内联函数处理。
多数情况下,以内联函数编译的函数会比那些被单独调用的函数运行得更快。
本节将演示一些非常具有特征的内联代码,以供读者研究。
指令清单43.3 字符串比较函数strcmp()
bool is_bool (char *s)
{
if (strcmp (s, "true")==0)
return true;
if (strcmp (s, "false")==0)
return false;
assert(0);
};
指令清单43.4 采用GCC 4.8.1优化的例子
.LC0:
.string "true"
.LC1:
.string "false"
is_bool:
.LFB0:
push edi
mov ecx, 5
push esi
mov edi, OFFSET FLAT:.LC0
sub esp, 20
mov esi, DWORD PTR [esp+32]
repz cmpsb
je .L3
mov esi, DWORD PTR [esp+32]
mov ecx, 6
mov edi, OFFSET FLAT:.LC1
repz cmpsb
seta cl
setb dl
xor eax, eax
cmp cl, dl
jne .L8
add esp, 20
pop esi
pop edi
ret
.L8:
mov DWORD PTR [esp], 0
call assert
add esp, 20
pop esi
pop edi
ret
.L3:
add esp, 20
mov eax, 1
pop esi
pop edi
ret
指令清单43.5 采用MSVC 2010优化的例子
$SG3454 DB 'true', 00H
$SG3456 DB 'false', 00H
_s$ = 8 ; size = 4
?is_bool@@YA_NPAD@Z PROC ; is_bool
push esi
mov esi, DWORD PTR _s$[esp]
mov ecx, OFFSET $SG3454 ; 'true'
mov eax, esi
npad 4 ; align next label
$LL6@is_bool:
mov dl, BYTE PTR [eax]
cmp dl, BYTE PTR [ecx]
jne SHORT $LN7@is_bool
test dl, dl
je SHORT $LN8@is_bool
mov dl, BYTE PTR [eax+1]
cmp dl, BYTE PTR [ecx+1]
jne SHORT $LN7@is_bool
add eax, 2
add ecx, 2
test dl, dl
jne SHORT $LL6@is_bool
$LN8@is_bool:
xor eax, eax
jmp SHORT $LN9@is_bool
$LN7@is_bool:
sbb eax, eax
sbb eax, -1
$LN9@is_bool:
test eax, eax
jne SHORT $LN2@is_bool
mov al, 1
pop esi
ret 0
$LN2@is_bool:
mov ecx, OFFSET $SG3456 ; 'false'
mov eax, esi
$LL10@is_bool:
mov dl, BYTE PTR [eax]
cmp dl, BYTE PTR [ecx]
jne SHORT $LN11@is_bool
test dl, dl
je SHORT $LN12@is_bool
mov dl, BYTE PTR [eax+1]
cmp dl, BYTE PTR [ecx+1]
jne SHORT $LN11@is_bool
add eax, 2
add ecx, 2
test dl, dl
jne SHORT $LL10@is_bool
$LN12@is_bool:
xor eax, eax
jmp SHORT $LN13@is_bool
$LN11@is_bool:
sbb eax, eax
sbb eax, -1
$LN13@is_bool:
test eax, eax
jne SHORT $LN1@is_bool
xor al, al
pop esi
ret 0
$LN1@is_bool:
push 11
push OFFSET $SG3458
push OFFSET $SG3459
call DWORD PTR __imp___wassert
add esp, 12
pop esi
ret 0
?is_bool@@YA_NPAD@Z ENDP ; is_bool
指令清单43.6 字符串长度函数strlen()的例子
int strlen_test(char *s1)
{
return strlen(s1);
};
指令清单43.7 采用MSVC 2010优化的例子
_s1$ = 8 ; size = 4
_strlen_test PROC
mov eax, DWORD PTR _s1$[esp-4]
lea edx, DWORD PTR [eax+1]
$LL3@strlen_tes:
mov cl, BYTE PTR [eax]
inc eax
test cl, cl
jne SHORT $LL3@strlen_tes
sub eax, edx
ret 0
_strlen_test ENDP
指令清单43.8 字符串复制函数strcpy()的例子
void strcpy_test(char *s1, char *outbuf)
{
strcpy(outbuf, s1);
};
指令清单43.9 采用MSVC 2010优化的例子
_s1$ = 8 ; size = 4
_outbuf$ = 12 ; size = 4
_strcpy_test PROC
mov eax, DWORD PTR _s1$[esp-4]
mov edx, DWORD PTR _outbuf$[esp-4]
sub edx, eax
npad 6 ; align next label
$LL3@strcpy_tes:
mov cl, BYTE PTR [eax]
mov BYTE PTR [edx+eax], cl
inc eax
test cl, cl
jne SHORT $LL3@strcpy_tes
ret 0
_strcpy_test ENDP
例子#1如下所示。
指令清单43.10 32字节的操作
#include <stdio.h>
void f(char *out)
{
memset(out, 0, 32);
};
在编译那些操作小体积内存块的memset()函数时,多数编译器不会分配标准的函数调用指令(call),反而会分配一堆的MOV指令,直接赋值。
指令清单43.11 64位下的GCC 4.9.1优化
f:
mov QWORD PTR [rdi], 0
mov QWORD PTR [rdi+8], 0
mov QWORD PTR [rdi+16], 0
mov QWORD PTR [rdi+24], 0
ret
这让我们想起了本书14.1.4节介绍的循环展开技术。
例子#2如下所示。
指令清单43.12 67字节内存的操作
#include <stdio.h>
void f(char *out)
{
memset(out, 0, 67);
};
当内存块的大小不是4或者8的整数倍时,不同的编译器会有不同的处理方法。
比如说,MSVC 2012依旧会分配一串MOV指令。
指令清单43.13 64位下的MSVC 2012优化
out$ = 8
f PROC
xor eax, eax
mov QWORD PTR [rcx], rax
mov QWORD PTR [rcx+8], rax
mov QWORD PTR [rcx+16], rax
mov QWORD PTR [rcx+24], rax
mov QWORD PTR [rcx+32], rax
mov QWORD PTR [rcx+40], rax
mov QWORD PTR [rcx+48], rax
mov QWORD PTR [rcx+56], rax
mov WORD PTR [rcx+64], ax
mov BYTE PTR [rcx+66], al
ret 0
f ENDP
GCC还会分配REP STOSQ指令。这可能比一堆的MOV赋值指令更短,效率更高。
指令清单43.14 64位下的GCC 4.9.1优化
f:
mov QWORD PTR [rdi], 0
mov QWORD PTR [rdi+59], 0
mov rcx, rdi
lea rdi, [rdi+8]
xor eax, eax
and rdi, -8
sub rcx, rdi
add ecx, 67
shr ecx, 3
rep stosq
ret
在编译那些复制小尺寸内存块的memcpy()函数时,多数编译器会分配一系列的MOV指令。
指令清单43.15 内存复制函数memcpy()
void memcpy_7(char *inbuf, char *outbuf)
{
memcpy(outbuf+10, inbuf, 7);
};
指令清单43.16 采用MSVC 2010优化
_inbuf$ = 8 ; size = 4
_outbuf$ = 12 ; size = 4
_memcpy_7 PROC
mov ecx, DWORD PTR _inbuf$[esp-4]
mov edx, DWORD PTR [ecx]
mov eax, DWORD PTR _outbuf$[esp-4]
mov DWORD PTR [eax+10], edx
mov dx, WORD PTR [ecx+4]
mov WORD PTR [eax+14], dx
mov cl, BYTE PTR [ecx+6]
mov BYTE PTR [eax+16], cl
ret 0
_memcpy_7 ENDP
指令清单43.17 采用GCC 4.8.1优化
memcpy_7:
push ebx
mov eax, DWORD PTR [esp+8]
mov ecx, DWORD PTR [esp+12]
mov ebx, DWORD PTR [eax]
lea edx, [ecx+10]
mov DWORD PTR [ecx+10], ebx
movzx ecx, WORD PTR [eax+4]
mov WORD PTR [edx+4], cx
movzx eax, BYTE PTR [eax+6]
mov BYTE PTR [edx+6], al
pop ebx
ret
上述指令的操作流程是:首先复制4个字节,然后复制一个字(也就是2个字节),接着复制最后一个字节。
此外,编译器还会通过赋值指令MOV复制结构体(structure)型数据。详情请参见本书的21.4.1节。
大尺寸内存块的操作:
不同的编译器会有不同的指令分配方案。
指令清单43.18 memcpy()内存复制的例子(这里列出了2个不同的例子,
一个是128字节的操作,另外一个则是123字节的内存操作)
void memcpy_128(char *inbuf, char *outbuf)
{
memcpy(outbuf+10, inbuf, 128);
};
void memcpy_123(char *inbuf, char *outbuf)
{
memcpy(outbuf+10, inbuf, 123);
};
MSVC分配了单条MOVSD指令。在循环控制变量ECX的配合下,MOVSD可一步完成128个字节的数据复制。其原因显然是128能被4整除。
指令清单43.19 MSVC 2010优化
_inbuf$ = 8 ; size = 4
_outbuf$ = 12 ; size = 4
_memcpy_128 PROC
push esi
mov esi, DWORD PTR _inbuf$[esp]
push edi
mov edi, DWORD PTR _outbuf$[esp+4]
add edi, 10
mov ecx, 32
rep movsd
pop edi
pop esi
ret 0
_memcpy_128 ENDP
在复制123个字节的程序里,编译器首先通过MOVSD指令复制30个32[1]字节(也就是120字节),然后依次通过MOVSW指令和MOVSB指令复制2个字节和1个字节。
指令清单43.20 采用MSVC 2010优化指令
_inbuf$ = 8 ; size = 4
_outbuf$ = 12 ; size = 4
_memcpy_123 PROC
push esi
mov esi, DWORD PTR _inbuf$[esp]
push edi
mov edi, DWORD PTR _outbuf$[esp+4]
add edi, 10
mov ecx, 30
rep movsd
movsw
movsb
pop edi
pop esi
ret 0
_memcpy_123 ENDP
GCC则分配了一个大型的通用的函数。这个函数适用于任意大小的内存块复制操作。
指令清单43.21 采用GCC 4.8.1优化
memcpy_123:
.LFB3:
push edi
mov eax, 123
push esi
mov edx, DWORD PTR [esp+16]
mov esi, DWORD PTR [esp+12]
lea edi, [edx+10]
test edi, 1
jne .L24
test edi, 2
jne .L25
.L7:
mov ecx, eax
xor edx, edx
shr ecx, 2
test al, 2
rep movsd
je .L8
movzx edx, WORD PTR [esi]
mov WORD PTR [edi], dx
mov edx, 2
.L8:
test al, 1
je .L5
movzx eax, BYTE PTR [esi+edx]
mov BYTE PTR [edi+edx], al
.L5:
pop esi
pop edi
ret
.L24:
movzx eax, BYTE PTR [esi]
lea edi, [edx+11]
add esi, 1
test edi, 2
mov BYTE PTR [edx+10], al
mov eax, 122
je .L7
.L25:
movzx edx, WORD PTR [esi]
add edi, 2
add esi, 2
sub eax, 2
mov WORD PTR [edi-2], dx
jmp .L7
.LFE3:
通用内存复制函数通常的工作原理如下:首先计算块有多少个字(32位),然后用MOVSD指令复制这些内存块,然后逐一复制剩余的字节。
更为复杂的内存复制函数则会利用SIMD指令集进行复制,这种复制还会涉及内存地址对齐的问题。有兴趣的读者可以参阅本书第25章的第2节。
指令清单43.22 memcmp()函数的例子
void memcpy_1235(char *inbuf, char *outbuf)
{
memcpy(outbuf+10, inbuf, 1235);
};
无论内存块的尺寸是多大,MSVC 2010都会插入相同的通用比较函数。
指令清单43.23 MSVC 2010优化程序
_buf1$ = 8 ; size = 4
_buf2$ = 12 ; size = 4
_memcmp_1235 PROC
mov edx, DWORD PTR _buf2$[esp-4]
mov ecx, DWORD PTR _buf1$[esp-4]
push esi
push edi
mov esi, 1235
add edx, 10
$LL4@memcmp_123:
mov eax, DWORD PTR [edx]
cmp eax, DWORD PTR [ecx]
jne SHORT $LN10@memcmp_123
sub esi, 4
add ecx, 4
add edx, 4
cmp esi, 4
jae SHORT $LL4@memcmp_123
$LN10@memcmp_123:
movzx edi, BYTE PTR [ecx]
movzx eax, BYTE PTR [edx]
sub eax, edi
jne SHORT $LN7@memcmp_123
movzx eax, BYTE PTR [edx+1]
movzx edi, BYTE PTR [ecx+1]
sub eax, edi
jne SHORT $LN7@memcmp_123
movzx eax, BYTE PTR [edx+2]
movzx edi, BYTE PTR [ecx+2]
sub eax, edi
jne SHORT $LN7@memcmp_123
cmp esi, 3
jbe SHORT $LN6@memcmp_123
movzx eax, BYTE PTR [edx+3]
movzx ecx, BYTE PTR [ecx+3]
sub eax, ecx
$LN7@memcmp_123:
sar eax, 31
pop edi
or eax, 1
pop esi
ret 0
$LN6@memcmp_123:
pop edi
xor eax, eax
pop esi
ret 0
_memcmp_1235 ENDP
笔者编写了一个检索、收缩(folding)常见内联函数的IDA脚本。有兴趣的读者请访问:
https://github.com/yurichev/IDA_scripts
[1] 应该为4个字节。