全局常量优化和符号插入

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_globalret_42 地址直到加载时才知道,我希望 ret_globalret_42 成为过程链接表 (PLT) 的一部分。这将在加载期间修复。 @MichaelPetch 我是个偶像。我的 liboverride 应该是 #define SCOPE SCOPE const struct wrap_ const int x; ptr = 60 ; SCOPE struct wrap const struct wrap_ *ptr; const w = &amp;ptr ; 来覆盖变量,而不是函数。但是在所有情况下我都得到 42。我想知道变量是否也不应该是可插入的。 @MichaelPetch 我会修改这个问题,因为还没有人在答案中解决它。 @MichaelPetch 所以现在的问题是:extern globals 不应该像 extern fucntions 一样总是可以插入的吗? @PSkocik,让他们不要const :) 0000000000001100 &lt;ret_global&gt;: 1100: 48 8b 05 e1 2e 00 00 mov 0x2ee1(%rip),%rax # 3fe8 &lt;w@@Base-0x40&gt; 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)和全局变量ptrw,但它没有定义int ret_global(void)

所以int ret_global(void)将从lib.so加载,这个函数将直接返回42,因为编译器认为lib.c中的ptrw不可能在运行时被修改(它们将将 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) 是可重载的,而 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? 。

【讨论】:

【参考方案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_resultret_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-&gt;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

第三步,编译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

    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:mainlib_override.so 调用 ret_42,因为它是预加载的,ret_42 现在解析为 lib_override.so 中的一个。

对于 2:main 从调用 ret_42lib.so 调用 ret_fn_result,但从 lib_override.so 调用,因为它现在解析为 lib_override.so 中的一个。

对于 3:mainlib.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 可能不知道。 不管怎样,问题进行了,所以作为答案。

以上是关于全局常量优化和符号插入的主要内容,如果未能解决你的问题,请参考以下文章

react native picker怎么插入多行

逆向课程第五讲逆向中的优化方式,除法原理,以及除法优化下

字面常量和符号常量是啥?

C语言里符号常量和常变量有啥区别?

C语言里符号常量和常变量有啥区别?

字面常量和符号常量的区别