第43章 内联函数

在编译阶段,将会被编译器把函数体展开并嵌入到每一个调用点的函数,就是内联函数。

指令清单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”强制编译器进行这种“嵌入”处理。

43.1 字符串和内存操作函数

在处理字符串和内存操作的常见函数时,例如:strycpy()、strcmp()、strlen、memset()、memcpy()、memcmp()函数,编译器通常会把这些函数当作内联函数处理。

多数情况下,以内联函数编译的函数会比那些被单独调用的函数运行得更快。

本节将演示一些非常具有特征的内联代码,以供读者研究。

43.1.1 字符串比较函数strcmp()

指令清单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.1.2 字符串长度函数strlen()

指令清单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.1.3 字符串复制函数strcpy()

指令清单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
43.1.4 内存设置函数memset()

例子#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
43.1.5 内存复制函数memcpy()

在编译那些复制小尺寸内存块的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.1.6 内存对比函数 memcmp()

指令清单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
43.1.7 IDA脚本

笔者编写了一个检索、收缩(folding)常见内联函数的IDA脚本。有兴趣的读者请访问:

https://github.com/yurichev/IDA_scripts


[1] 应该为4个字节。