我正在尝试使用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
处于启用状态,否则所有全局变量都始终可插入,那将是很好的选择,但一个人只能希望。)
答案 0 :(得分:2)
根据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
,因为编译器认为{的ptr
和w
不可能{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.
能够内联复杂const变量和结构的编译器常量折叠优化
函数的编译器默认行为是导出。如果未使用-fvisibility=hidden
标志,则将导出所有功能。由于任何定义的函数都将导出,因此无法进行内联。因此无法内嵌对ret_42
中的ret_fn_result
的调用。打开-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
第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.so
,lib_override.so
和a.out
。
我们简单地拨打a.out
:
现在让我们预载lib_override.so
:
对于1:main
从ret_42
调用lib_override.so
,因为它已预先加载,ret_42
现在解析为lib_override.so
中的一个。
对于2:main
从ret_fn_result
调用lib.so
到ret_42
调用lib_override.so
的{{1}},因为它现在解析为lib_override.so
中的一个
对于3:main
从ret_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