全局const优化和符号插入

时间:2018-10-27 11:07:40

标签: c optimization linker compiler-construction 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; }

返回一个中间常数。

事实证明他们可以:

0000000000000010 <ret_global>:
   10:  b8 2a 00 00 00          mov    $0x2a,%eax
   15:  c3                      retq   

但是令人惊讶的是,去除静态会产生相同的程序集输出。 令我感到好奇的是,如果全局变量不是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

输出

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

编译器是否可以将refs替换为具有中间体的外部全局变量?这些不是也可以插入吗?


编辑:

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

我一直认为这是为了允许符号插入。顺带一提,c确实对其进行了优化。

我想知道它在哪里(如果有的话)说对extern const w中的ret_global()的引用可以优化为中间,而对ret_42()中的ret_fn_result的调用却不能。

无论如何,除非您建立翻译单元边界,否则在不同的编译器中,符号迭代似乎完全不一致且不可靠。 :/ (如果除非-fno-semantic-interposition处于启用状态,否则所有全局变量都始终可插入,那将是很好的选择,但一个人只能希望。)

3 个答案:

答案 0 :(得分:2)

根据What is the LD_PRELOAD trick? LD_PRELOAD是一个环境变量,允许用户在加载任何其他库(包括libc.so)之前加载库。

根据此定义,它意味着两件事:

  1. LD_PRELOAD中指定的库可以重载其他库中的符号。

  2. 但是,如果指定的库不包含该符号,则将照常在其他库中搜索该符号。

您在此处将LD_PRELOAD指定为lib_override.so,它定义了int ret_42(void)以及全局变量ptrw,但没有 >定义int ret_global(void)

因此将从int ret_global(void)加载lib.so,并且此函数将直接返回42,因为编译器认为{的ptrw不可能{1}}可以在运行时进行修改(它们将被放置在lib.c中的int const data节中,elf确保不能在运行时通过硬件内存保护对其进行修改),因此编译器对此进行了优化,以直接返回linux

编辑-测试:

所以我对您的脚本做了一些修改:

42

这一次,它打印:

#!/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; }
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 };
int ret_global(void) { return w.ptr->x; }
EOF

cat > main.c <<EOF
#include <stdio.h>
int ret_42(void), ret_global(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_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

编辑-结论:

因此,事实证明您要么过载所有相关零件,要么什么都不过载,否则您将得到这种棘手的行为。另一种方法是在标头中而不是在动态库中定义ret_42()=42 ret_global()=42 w.ptr->x=42 ret_42()=50 ret_global()=60 w.ptr->x=60 ,因此当您尝试重载某些功能来进行一些测试时,您不必担心。

编辑-解释为什么int ret_global(void)可以重载,而int ret_global(void)ptr不能重载。

首先,我想指出您定义的符号类型(使用How do I list the symbols in a .so file 中的技术:

文件w

lib.so

文件Symbol table '.dynsym' contains 13 entries: Num: Value Size Type Bind Vis Ndx Name 5: 0000000000001110 6 FUNC GLOBAL DEFAULT 12 ret_global 6: 0000000000001120 17 FUNC GLOBAL DEFAULT 12 ret_fn_result 7: 000000000000114c 0 FUNC GLOBAL DEFAULT 14 _fini 8: 0000000000001100 6 FUNC GLOBAL DEFAULT 12 ret_42 9: 0000000000000200 4 OBJECT GLOBAL DEFAULT 1 ptr 10: 0000000000003018 8 OBJECT GLOBAL DEFAULT 22 w Symbol table '.symtab' contains 28 entries: Num: Value Size Type Bind Vis Ndx Name 23: 0000000000001100 6 FUNC GLOBAL DEFAULT 12 ret_42 24: 0000000000001110 6 FUNC GLOBAL DEFAULT 12 ret_global 25: 0000000000001120 17 FUNC GLOBAL DEFAULT 12 ret_fn_result 26: 0000000000003018 8 OBJECT GLOBAL DEFAULT 22 w 27: 0000000000000200 4 OBJECT GLOBAL DEFAULT 1 ptr

lib_override.so

您会发现,尽管两个函数都是Symbol table '.dynsym' contains 11 entries: Num: Value Size Type Bind Vis Ndx Name 6: 0000000000001100 6 FUNC GLOBAL DEFAULT 12 ret_42 7: 0000000000000200 4 OBJECT GLOBAL DEFAULT 1 ptr 8: 0000000000001108 0 FUNC GLOBAL DEFAULT 13 _init 9: 0000000000001120 0 FUNC GLOBAL DEFAULT 14 _fini 10: 0000000000003018 8 OBJECT GLOBAL DEFAULT 22 w Symbol table '.symtab' contains 26 entries: Num: Value Size Type Bind Vis Ndx Name 23: 0000000000001100 6 FUNC GLOBAL DEFAULT 12 ret_42 24: 0000000000003018 8 OBJECT GLOBAL DEFAULT 22 w 25: 0000000000000200 4 OBJECT GLOBAL DEFAULT 1 ptr 符号,但所有函数都标记为GLOBAL类型,可重载,而所有变量的类型均为FUNC。类型OBJECT表示它不可重载,因此编译器不需要使用符号解析来获取数据。

有关此的更多信息,请检查以下内容:What Are "Tentative" Symbols?

答案 1 :(得分:1)

您可以使用LD_DEBUG=bindings来跟踪符号绑定。在这种情况下,它会打印(以及其他内容):

 17570: binding file /tmp/lib.so [0] to /tmp/lib_override.so [0]: normal symbol `ptr'
 17570: binding file /tmp/lib_override.so [0] to /tmp/lib_override.so [0]: normal symbol `ptr'
 17570: binding file ./a.out [0] to /tmp/lib_override.so [0]: normal symbol `ret_42'
 17570: binding file ./a.out [0] to /tmp/lib_override.so [0]: normal symbol `ret_global'

因此确实插入了ptr中的lib.so对象,但是主程序从未在原始库中调用ret_global。调用会从预加载的库转到ret_global,因为该函数也已插入。

答案 2 :(得分:0)

编辑:问题: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.

TLDR;这种行为的逻辑(至少对于GCC)

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

  • 函数的编译器默认行为是导出。如果未使用-fvisibility=hidden标志,则将导出所有功能。由于任何定义的函数都将导出,因此无法进行内联。因此无法内嵌对ret_42中的ret_fn_result的调用。打开-fvisibility=hidden,结果将如下所示。

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

  • 对于该主题还有其他有效的标志。最著名的人:

    • -Bsymbolic-Bsymbolic-functions--dynamic-listper 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   

技术人员

步骤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; }

编译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

分别将ptrw放在rodatadata.rel.ro的位置(因为指针const)。不断折叠会产生以下代码:

0000000000001120 <ret_global>:
    1120:   b8 2a 00 00 00          mov    $0x2a,%eax
    1125:   c3                      retq   

另一部分是:

int ret_42(void) { return 42; }
int ret_fn_result(void) { return ret_42()+1; }

此处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   

考虑到编译器只知道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 };

这很简单:

$ 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

导出的函数ret_42,然后将ptrw分别放置到rodatadata.rel.ro(由于const指针)。不断折叠会产生以下代码:

00000000000010f0 <ret_42>:
    10f0:   b8 32 00 00 00          mov    $0x32,%eax
    10f5:   c3                      retq

第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

所有符号均未定义。所以它们必须来自某个地方。

然后默认情况下,我们用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>

现在我们手中有lib.solib_override.soa.out

我们简单地拨打a.out

  1. main => ret_42 => lib.so => ret_42 =>返回42
  2. main => ret_fn_result => lib.so => ret_fn_result => return(lib.so => ret_42 => return 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. main => ret_42 => lib_override.so => ret_42 =>返回50
  2. main => ret_fn_result => lib.so => ret_fn_result => return(lib_override.so => ret_42 => return 50)+ 1
  3. main => ret_global => lib.so => ret_global =>返回rodata 42
  4. main => lib_override.so => w.ptr-> x = rodata 60

对于1:mainret_42调用lib_override.so,因为它已预先加载,ret_42现在解析为lib_override.so中的一个。

对于2:mainret_fn_result调用lib.soret_42调用lib_override.so的{​​{1}},因为它现在解析为lib_override.so中的一个

对于3:mainret_global调用lib.so,返回折叠常数42。

对于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