全局const优化和符号插入

问题描述 投票:6回答:3

我正在尝试使用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到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

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

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

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

c optimization linker compiler-construction elf
3个回答
2
投票

根据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,因为编译器看不到ptrwlib.c可能在运行时被修改(它们将被放入data中的int const 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)可以超载,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?


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,因为函数也是插入的。


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; Logic behind this behavior (at least for 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   

Technicals

步骤#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 =玫瑰花结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,因为它现在解析为lib_override.so中的一个。

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

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

最后,一旦使用内联的折叠常数生成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
© www.soinside.com 2019 - 2024. All rights reserved.