全局const优化和符号插入

PSk*_*cik 6 c compiler-construction optimization linker elf

我正在尝试使用gcc和clang来查看它们是否可以优化

#define SCOPE static
SCOPE const struct wrap_ { const int x; } ptr = { 42 /*==0x2a*/ };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
Run Code Online (Sandbox Code Playgroud)

返回一个中间常量.

事实证明他们可以:

0000000000000010 <ret_global>:
   10:  b8 2a 00 00 00          mov    $0x2a,%eax
   15:  c3                      retq   
Run Code Online (Sandbox Code Playgroud)

但令人惊讶的是,移除静态会产生相同的装配输出.这让我很好奇,因为如果全局不是static它应该是可插入的并且用中间体替换引用应该防止全局变量上的位置.

确实如此:

#!/bin/sh -eu
: ${CC:=gcc}
cat > lib.c <<EOF
int ret_42(void) { return 42; }

#define SCOPE 
SCOPE const struct wrap_ { const int x; } ptr = { 42 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
int ret_fn_result(void) { return ret_42()+1; }
EOF

cat > lib_override.c <<EOF
int ret_42(void) { return 50; }

#define SCOPE
 SCOPE const struct wrap_ { const int x; } ptr = { 60 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
EOF

cat > main.c <<EOF
#include <stdio.h>
int ret_42(void), ret_global(void), ret_fn_result(void);
struct wrap_ { const int x; };
extern struct wrap { const struct wrap_ *ptr; } const w;
int main(void)
{
    printf("ret_42()=%d\n", ret_42());
    printf("ret_fn_result()=%d\n", ret_fn_result());
    printf("ret_global()=%d\n", ret_global());
    printf("w.ptr->x=%d\n",w.ptr->x);
}
EOF
for c in *.c; do
    $CC -fpic -O2 $c -c
    #$CC -fpic -O2 $c -c -fno-semantic-interposition
done
$CC lib.o -o lib.so -shared
$CC lib_override.o -o lib_override.so -shared
$CC main.o $PWD/lib.so
export LD_LIBRARY_PATH=$PWD
./a.out
LD_PRELOAD=$PWD/lib_override.so ./a.out
Run Code Online (Sandbox Code Playgroud)

输出

ret_42()=42
ret_fn_result()=43
ret_global()=42
w.ptr->x=42
ret_42()=50
ret_fn_result()=51
ret_global()=42
w.ptr->x=60
Run Code Online (Sandbox Code Playgroud)

编译器可以用中间件替换refs到extern全局变量吗?这些也不应该是可插入的吗?


编辑:

GCC并没有优化出外部函数调用(除非编译-fno-semantic-interposition)
诸如呼叫到ret_42()int ret_fn_result(void) { return ret_42()+1; },即使,与所涉及的参考extern global const变量,符号改变的定义的唯一方法是通过插入.

  0000000000000020 <ret_fn_result>:
  20:   48 83 ec 08             sub    $0x8,%rsp
  24:   e8 00 00 00 00          callq  29 <ret_fn_result+0x9>
  29:   48 83 c4 08             add    $0x8,%rsp
  2d:   83 c0 01                add    $0x1,%eax
Run Code Online (Sandbox Code Playgroud)

我一直认为这是为了允许符号插入的可能性.顺便说一句,clang确实优化了它们.

我想知道在哪里(如果有的话)它表示对extern const win 的引用ret_global()可以优化为中间,而对ret_42()in 的调用ret_fn_result则不能.

无论如何,除非你建立翻译单元边界,否则看起来符号iterposition在不同编译器之间是非常不一致和不可靠的.:/(如果只是所有全局变量一直是可插入的,除非-fno-semantic-interposition打开,否则会很好.但是人们只能希望.)

mur*_*adm 1

编辑:问题:I wonder where (if anywhere) it says that the reference to extern const w in ret_global() can be optimized to an intermediate while the call to ret_42() in ret_fn_result cannot.

太长了;这种行为背后的逻辑(至少对于 GCC 而言)

  • 编译器常量折叠优化能够内联复杂的 const 变量和结构

  • 函数的编译器默认行为是导出。如果-fvisibility=hidden未使用标志,则导出所有函数。因为任何定义的函数都是导出的,所以不能内联。所以对ret_42in的调用ret_fn_result不能内联。打开-fvisibility=hidden,结果如下。

  • 比方说,如果可以同时出于优化目的导出和内联函数,则将导致linker创建有时以一种方式工作(内联)、有时被覆盖(插入)、有时直接工作的代码在生成的可执行文件的单次加载和执行范围内。

  • 还有其他对此主题有效的标志。最值得注意的:

    • -Bsymbolic-Bsymbolic-functions--dynamic-list按照SO

    • -fno-semantic-interposition

    • 当然优化标志

ret_fn_result隐藏函数ret_42,不导出然后内联。

0000000000001110 <ret_fn_result>:
    1110:   b8 2b 00 00 00          mov    $0x2b,%eax
    1115:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

技术资料

步骤#1,主题定义如下lib.c

SCOPE const struct wrap_ { const int x; } ptr = { 42 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
Run Code Online (Sandbox Code Playgroud)

lib.c编译时,w.ptr->x优化为const. 因此,通过不断折叠,会导致:

$ object -T lib.so
lib.so:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000000000  w   D  *UND*  0000000000000000              __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000  GLIBC_2.2.5 __cxa_finalize
0000000000001110 g    DF .text  0000000000000006  Base        ret_42
0000000000002000 g    DO .rodata    0000000000000004  Base        ptr
0000000000001120 g    DF .text  0000000000000006  Base        ret_global
0000000000001130 g    DF .text  0000000000000011  Base        ret_fn_result
0000000000003e18 g    DO .data.rel.ro   0000000000000008  Base        w
Run Code Online (Sandbox Code Playgroud)

其中ptr和分别w放置到rodatadata.rel.ro(因为const指针)。常量折叠会产生以下代码:

0000000000001120 <ret_global>:
    1120:   b8 2a 00 00 00          mov    $0x2a,%eax
    1125:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

另一部分是:

int ret_42(void) { return 42; }
int ret_fn_result(void) { return ret_42()+1; }
Run Code Online (Sandbox Code Playgroud)

ret_42是一个函数,由于没有隐藏,所以它是导出函数。所以它是一个code. 两者都会导致:

0000000000001110 <ret_42>:
    1110:   b8 2a 00 00 00          mov    $0x2a,%eax
    1115:   c3                      retq   

0000000000001130 <ret_fn_result>:
    1130:   48 83 ec 08             sub    $0x8,%rsp
    1134:   e8 f7 fe ff ff          callq  1030 <ret_42@plt>
    1139:   48 83 c4 08             add    $0x8,%rsp
    113d:   83 c0 01                add    $0x1,%eax
    1140:   c3                      retq   
Run Code Online (Sandbox Code Playgroud)

考虑到编译器只知道lib.c,我们就完成了。放在lib.so一边。

步骤#2,编译lib_override.c

int ret_42(void) { return 50; }

#define SCOPE
SCOPE const struct wrap_ { const int x; } ptr = { 60 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
Run Code Online (Sandbox Code Playgroud)

这很简单:

$ objdump -T lib_override.so
lib_override.so:     file format elf64-x86-64

DYNAMIC SYMBOL TABLE:
0000000000000000  w   D  *UND*  0000000000000000              _ITM_deregisterTMCloneTable
0000000000000000  w   D  *UND*  0000000000000000              __gmon_start__
0000000000000000  w   D  *UND*  0000000000000000              _ITM_registerTMCloneTable
0000000000000000  w   DF *UND*  0000000000000000  GLIBC_2.2.5 __cxa_finalize
00000000000010f0 g    DF .text  0000000000000006  Base        ret_42
0000000000002000 g    DO .rodata    0000000000000004  Base        ptr
0000000000003e58 g    DO .data.rel.ro   0000000000000008  Base        w
Run Code Online (Sandbox Code Playgroud)

导出函数ret_42,然后ptr和分别w放入rodatadata.rel.ro(因为const指针)。常量折叠会产生以下代码:

00000000000010f0 <ret_42>:
    10f0:   b8 32 00 00 00          mov    $0x32,%eax
    10f5:   c3                      retq
Run Code Online (Sandbox Code Playgroud)

STEP 3,编译main.c,先看对象:

$ objdump -t main.o

# SKIPPED

0000000000000000         *UND*  0000000000000000 _GLOBAL_OFFSET_TABLE_
0000000000000000         *UND*  0000000000000000 ret_42
0000000000000000         *UND*  0000000000000000 printf
0000000000000000         *UND*  0000000000000000 ret_fn_result
0000000000000000         *UND*  0000000000000000 ret_global
0000000000000000         *UND*  0000000000000000 w
Run Code Online (Sandbox Code Playgroud)

我们所有的符号都未定义。所以他们必须来自某个地方。

然后我们默认链接lib.so,代码为(printf等省略):

0000000000001070 <main>:
    1074:   e8 c7 ff ff ff          callq  1040 <ret_42@plt>
    1089:   e8 c2 ff ff ff          callq  1050 <ret_fn_result@plt>
    109e:   e8 bd ff ff ff          callq  1060 <ret_global@plt>
    10b3:   48 8b 05 2e 2f 00 00    mov    0x2f2e(%rip),%rax        # 3fe8 <w>
Run Code Online (Sandbox Code Playgroud)

现在我们手里有lib.solib_override.so和。a.out

让我们简单地调用a.out

  1. 主要=> ret_42 => lib.so => ret_42 => 返回42
  2. 主要 => ret_fn_result => lib.so => ret_fn_result => 返回 ( lib.so => ret_42 => 返回 42 ) + 1
  3. main => ret_global => lib.so => ret_global => 返回rodata 42
  4. main => lib.so => w.ptr->x = rodata 42

现在让我们预加载lib_override.so

  1. 主要=> ret_42 => lib_override.so => ret_42 => 返回50
  2. 主要 => ret_fn_result => lib.so => ret_fn_result => 返回 ( lib_override.so => ret_42 => 返回 50 ) + 1
  3. main => ret_global => lib.so => ret_global => 返回rodata 42
  4. 主要=> lib_override.so => w.ptr->x = rodata 60

对于 1:main调用ret_42fromlib_override.so因为它是预加载的,ret_42现在解析为 1 in lib_override.so

对于 2:main调用ret_fn_resultfrom lib.sowhich 调用ret_42但 from lib_override.so,因为它现在解析为 1 in lib_override.so

对于3:main调用返回折叠常量 42。ret_globallib.so

对于4:main读取指向 的外部指针lib_override.so,因为它是预加载的。

最后,一旦lib.so用内联的折叠常量生成,就不能要求它们是“可覆盖的”。如果打算拥有可重写的数据结构,则应该以其他方式定义它(提供操作它们的函数,不要使用常量等)。因为当将某些东西定义为常量时,意图是明确的,并且编译器会执行它所做的操作。那么即使相同的符号在main.c或其他地方被定义为非常量,它也不能unfolded回到 中lib.c


#!/bin/sh -eu
: ${CC:=gcc}
cat > lib.c <<EOF
int ret_42(void) { return 42; }

#define SCOPE 
SCOPE const struct wrap_ { const int x; } ptr = { 42 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
int ret_global(void) { return w.ptr->x; }
int ret_fn_result(void) { return ret_42()+1; }
EOF

cat > lib_override.c <<EOF
int ret_42(void) { return 50; }

#define SCOPE
 SCOPE const struct wrap_ { const int x; } ptr = { 60 };
SCOPE struct wrap { const struct wrap_ *ptr; } const w = { &ptr };
EOF

cat > main.c <<EOF
#include <stdio.h>
int ret_42(void), ret_global(void), ret_fn_result(void);
struct wrap_ { const int x; };
extern struct wrap { const struct wrap_ *ptr; } const w;
int main(void)
{
    printf("ret_42()=%d\n", ret_42());
    printf("ret_fn_result()=%d\n", ret_fn_result());
    printf("ret_global()=%d\n", ret_global());
    printf("w.ptr->x=%d\n",w.ptr->x);
}
EOF
for c in *.c; do gcc -fpic -O2 $c -c; done
$CC lib.o -o lib.so -shared 
$CC lib_override.o -o lib_override.so -shared
$CC main.o $PWD/lib.so
export LD_LIBRARY_PATH=$PWD
./a.out
LD_PRELOAD=$PWD/lib_override.so ./a.out
Run Code Online (Sandbox Code Playgroud)