全局常量优化和符号插入
Posted
技术标签:
【中文标题】全局常量优化和符号插入【英文标题】:Global const optimization and symbol interposition 【发布时间】:2019-03-31 22:55:41 【问题描述】:我正在试验 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
编译)
例如在int ret_fn_result(void) return ret_42()+1;
中对ret_42()
的调用,尽管与对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
我一直认为这是为了允许插入符号。顺便说一句,clang 确实优化了它们。
我想知道它在哪里(如果有的话)说 ret_global()
中对 extern const w
的引用可以优化为中间体,而 ret_fn_result
中对 ret_42()
的调用不能。
无论如何,除非您建立翻译单元边界,否则符号迭代似乎在不同编译器之间非常不一致且不可靠。 :/
(如果所有全局变量都可以始终插入,除非-fno-semantic-interposition
开启,但我们只能希望。)
【问题讨论】:
由于ret_global
和 ret_42
地址直到加载时才知道,我希望 ret_global
和 ret_42
成为过程链接表 (PLT) 的一部分。这将在加载期间修复。
@MichaelPetch 我是个偶像。我的 liboverride 应该是 #define SCOPE SCOPE const struct wrap_ const int x; ptr = 60 ; SCOPE struct wrap const struct wrap_ *ptr; const w = &ptr ;
来覆盖变量,而不是函数。但是在所有情况下我都得到 42。我想知道变量是否也不应该是可插入的。
@MichaelPetch 我会修改这个问题,因为还没有人在答案中解决它。
@MichaelPetch 所以现在的问题是:extern globals 不应该像 extern fucntions 一样总是可以插入的吗?
@PSkocik,让他们不要const
:) 0000000000001100 <ret_global>: 1100: 48 8b 05 e1 2e 00 00 mov 0x2ee1(%rip),%rax # 3fe8 <w@@Base-0x40> 1107: 48 8b 00 mov (%rax),%rax 110a: 8b 00 mov (%rax),%eax 110c: c3 retq
【参考方案1】:
根据What is the LD_PRELOAD trick?
,LD_PRELOAD
是一个环境变量,允许用户在加载任何其他库之前加载库,包括libc.so
。
从这个定义来看,它意味着两件事:
LD_PRELOAD
中指定的库可以重载其他库中的符号。
但是,如果指定的库不包含该符号,则将照常在其他库中搜索该符号。
这里你将LD_PRELOAD
指定为lib_override.so
,它定义了int ret_42(void)
和全局变量ptr
和w
,但它没有定义int ret_global(void)
。
所以int ret_global(void)
将从lib.so
加载,这个函数将直接返回42
,因为编译器认为lib.c
中的ptr
和w
不可能在运行时被修改(它们将将 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
和 w
不是。
首先,我想指出您定义的符号类型(使用来自How do I list the symbols in a .so file 的技术:
文件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
类型。类型OBJECT
表示不可重载,因此编译器不需要使用符号解析来获取数据。
有关这方面的更多信息,请查看:What Are "Tentative" Symbols? 。
【讨论】:
【参考方案2】:您可以使用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'
所以lib.so
中的ptr
对象确实是插入的,但是主程序从不调用原库中的ret_global
。调用从预加载的库转到ret_global
,因为该函数也被插入。
【讨论】:
【参考方案3】:编辑:问题: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_fn_result
中调用ret_42
不能被内联。打开-fvisibility=hidden
,结果如下。
假设,如果可以同时导出和内联函数以进行优化,这将导致 linker
创建有时以一种方式(内联)工作的代码,有时工作覆盖(插入),有时直接在单个加载和执行生成的可执行文件的范围内工作。
还有其他对该主题有效的标志。最著名的:
-Bsymbolic
、-Bsymbolic-functions
和 --dynamic-list
为 per 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
ptr
和 w
分别放在 rodata
和 data.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
,然后将ptr
和w
分别放到rodata
和data.rel.ro
(因为const
指针)。常量折叠产生以下代码:
00000000000010f0 <ret_42>:
10f0: b8 32 00 00 00 mov $0x32,%eax
10f5: c3 retq
第三步,编译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.so
、lib_override.so
和a.out
。
让我们简单地拨打a.out
:
-
main => ret_42 => lib.so => ret_42 => return 42
main => ret_fn_result => lib.so => ret_fn_result => return (lib.so => ret_42 => return 42) + 1
main => ret_global => lib.so => ret_global => return rodata 42
main => lib.so => w.ptr->x = rodata 42
现在让我们预加载lib_override.so
:
-
main => ret_42 => lib_override.so => ret_42 => 返回 50
main => ret_fn_result => lib.so => ret_fn_result => return (lib_override.so => ret_42 => return 50) + 1
main => ret_global => lib.so => ret_global => return rodata 42
main => lib_override.so => w.ptr->x = rodata 60
对于 1:main
从 lib_override.so
调用 ret_42
,因为它是预加载的,ret_42
现在解析为 lib_override.so
中的一个。
对于 2:main
从调用 ret_42
的 lib.so
调用 ret_fn_result
,但从 lib_override.so
调用,因为它现在解析为 lib_override.so
中的一个。
对于 3:main
从 lib.so
调用 ret_global
,返回折叠常量 42。
对于 4:main
读取指向 lib_override.so
的外部指针,因为它是预加载的。
最后,一旦lib.so
使用内联的折叠常量生成,就不能要求它们“可覆盖”。如果打算拥有可覆盖的数据结构,则应该以其他方式定义它(提供操作它们的函数,不要使用常量等)。因为当将某些东西定义为常量时,意图是明确的,编译器会做它所做的事情。那么即使同一个符号在main.c
或其他地方被定义为非常量,在lib.c
中也不能是unfolded
。
#!/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
【讨论】:
它是作为动态库加载的,所以让const
变量具有不同的值并不是未定义的行为。
但是是的,如果修改,它是未定义的。 @muradm 但是 OP 没有尝试修改,您也没有解释为什么它会选择 int ret_global(void)
的那个版本。
@JiaHaoXu,这很明显,我们不是在讨论LD_PRELOAD
的工作原理。
是的,但 OP 可能不知道。
不管怎样,问题进行了,所以作为答案。以上是关于全局常量优化和符号插入的主要内容,如果未能解决你的问题,请参考以下文章