c - 全局 const 优化和符号插入

标签 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

编译器可以用中间体替换 extern 全局变量的 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 打开,但人们只能希望如此。)

最佳答案

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

从这个定义来看,它意味着两件事:

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

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

这里你指定LD_PRELOADlib_override.so,它定义了int ret_42(void)和全局变量ptrw,但它没有定义 int ret_global(void)

所以int ret_global(void)会从lib.so中加载,这个函数会直接返回42,因为编译器看不到lib.c 中的 ptrw 可以在运行时修改的可能性(它们将被放入 int const data elf中的section,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

编辑——结论:

所以事实证明,你要么重载所有相关部分,要么什么都不重载,否则你会遇到这种棘手的行为。另一种方法是在 header 中定义 int ret_global(void),而不是在动态库中,因此当您尝试重载某些功能来进行某些测试时,您不必担心这一点。

编辑 -- 解释为什么 int ret_global(void) 是可重载的而 ptrw 不是。

首先,我想指出您定义的符号类型(使用 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? .

关于c - 全局 const 优化和符号插入,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/53021281/

相关文章:

c++ - 多个已定义符号的 C/C++ 链接器顺序

c - 为什么文件复制程序中按回车有输出,没有到达EOF,书上的例子 "THE C PROGRAMMING LANGUGE BY DENNIS RITCHIE"

c - 将字符串解析为(long long)整数

c++ - 使 SASL(带有 GSSAPI)启用客户端和服务器并使用 LDAP+kerberos 作为身份验证器? (SSO 系统上的单点登录)

optimization - GCC -mthumb 反对 -marm

加载类时java运行时错误

c - 消息队列。消息发送 : Invalid argument

javascript - Google Optimize 不适用于激活事件(未设置页面位置)

xml - 优化原始 SOAP 请求和响应的一些有用技术是什么?

Android NDK 链接问题