当前位置: 首页 > 知识库问答 >
问题:

全局常数优化和品种介位

鲁洋
2023-03-14

我在用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   

但是令人惊讶的是,移除静态输出会产生相同的汇编输出。这让我很好奇,因为如果全局变量不是< code>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

编译器可以用中间变量替换外部全局变量的引用吗?这些不也应该被干预吗?

编辑:

Gcc 不会优化外部函数调用(除非使用 -fno-semantic-interposition 编译
),例如在 int ret_fn_result(void) { return ret_42() 1; } 中调用 ret_42(),即使与引用 extern 全局 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确实优化了它们。

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

无论如何,符号迭代似乎在不同的编译器之间非常不一致且不可靠,除非您建立转换单元边界。:/(如果只是所有全局变量都是可连续插入的,除非 -fno-semantic-interposition 处于打开状态,那就太好了,但人们只能希望。

共有3个答案

淳于熙云
2023-03-14

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

>

  • 编译器常量折叠优化,能够内联复杂常量变量和结构

    函数的编译器默认行为是导出。如果未使用 -fvisibility=hidden 标志,则导出所有函数。由于导出任何已定义的函数,因此无法内联该函数。因此,无法内联ret_fn_result对ret_42的呼叫。打开 -fvisibility=hidden,结果将如下所示。

    假设可以同时导出和内联函数以进行优化,则会导致链接器创建有时以一种方式(内联)工作的代码,有时工作被覆盖(插入),有时直接在单个加载和执行生成的可执行文件的范围内工作。

    还有其他标志对这个主题有效。最著名的人:

    >

  • -b符号-b符号函数-动态列表

    -fno-semantic-interposition

    当然,优化标志

    当< code>ret_42隐藏时,函数< code>ret_fn_result,不导出,然后内联。

    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; }
    

    当<code>lib。c被编译,w.ptr-

    $ 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
    

    其中<code>ptr(因为<code>常量

    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; }
    

    这里<code>ret_42</code>是一个函数,因为它不是隐藏的,所以它是导出函数。因此它是一个<code>代码</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   
    

    考虑到编译器只知道< code>lib.c,我们就完成了。将< code>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
    

    我们所有的符号都没有定义。所以他们必须来自某个地方。

    然后我们默认用< code>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在手。

    让我们简单地调用<code>a.out</code>:

      < li >主要=

    现在让我们用lib_override.so预加载:

      < li >主要=

    对于1:<code>main</code>从<code>lib_override调用<code>ret_42</code>。因此因为它是预加载的,ret_42现在解析为lib_override.so

    对于 2:调用ret_fn_result来自 lib.so,该调用ret_42,而是来自 lib_override.so,因为它现在解析为 lib_override.so 中的 1。

    对于3:<code>main</code>从<code>lib调用<code>ret_global</code>。因此返回折叠常数42。

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

    最后,一旦< code>lib.so是用内嵌的折叠常量生成的,就不能要求它们是“可重写的”。如果想要拥有可重写的数据结构,应该用其他方式定义它(提供操作它们的函数,不要使用常量等等)。).因为当定义一个东西为常量时,意图是明确的,编译器做什么就做什么。然后,即使该符号在< code>main.c或其他地方被定义为非常量,它也不能在< code>lib.c中被< code >展开回。

    #!/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
    

  • 彭兴朝
    2023-03-14

    您可以使用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,因为函数也被插入了。

    何烨华
    2023-03-14

    根据什么是LD_PRELOAD技巧?,LD_PRELOAD是一个环境变量,允许用户在加载任何其他库之前加载一个库,包括libc.so

    根据这个定义,它意味着两件事:

    > < Li > < p > < code > LD _ PRELOAD 中指定的库可以从其他库中重载符号。

    但是,如果指定的该库不包含该符号,则将像往常一样在其他库中搜索该符号。

    在这里,您将< code>LD_PRELOAD指定为< code>lib_override.so,它定义了< code>int ret_42(void)和全局变量< code>ptr和< code>w,但没有定义< code>int ret_global(void)。

    因此<code>int ret_global(void),此函数将直接返回<code>42,因为编译器不可能从<code>库中看到<code>ptr。c可以在运行时修改(它们将放在<code>elf。

    编辑-测试:

    所以我对你的剧本做了一些修改:

    #!/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
    

    编辑 -- 结论:

    所以事实证明,您要么重载所有相关部分,要么重载任何内容,否则您将获得如此棘手的行为。另一种方法是在标头中定义intret_global(val),而不是在动态库中,因此当您尝试重载一些功能来进行一些测试时,您不必担心这一点。

    编辑--解释为什么intret_global(val)是可重载的,而ptrw不是。

    首先,我想指出您定义的符号类型(使用如何在. so文件中列出符号的技术:

    文件< code>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
    

    文件<code>lib_override.so</code>:

    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 类型意味着它是不可重载的,因此编译器不需要使用符号解析来获取数据。

    关于这方面的进一步信息,请查看:什么是“暂定”符号?。

     类似资料:
    • 程序中的一切变量的初始值都直接或间接地依赖常量或常量表达式生成。在Go语言中很多变量是默认零值初始化的,但是Go汇编中定义的变量最好还是手工通过常量初始化。有了常量之后,就可以衍生定义全局变量,并使用常量组成的表达式初始化其它各种变量。本节将简单讨论Go汇编语言中常量和全局变量的用法。 3.3.1 常量 Go汇编语言中常量以$美元符号为前缀。常量的类型有整数常量、浮点数常量、字符常量和字符串常量等

    • 本文向大家介绍PHP超全局数组(Superglobals)介绍,包括了PHP超全局数组(Superglobals)介绍的使用技巧和注意事项,需要的朋友参考一下 概述 想做一个微信的公众平台,阅读了微信官方给的网址接入的示例代码,发现有个问题好像一直都是半知半解的,就是在类里边直接使用$_GET。仔细查了下关于这方面的知识,发现PHP中这部分的基础知识掌握的不是很牢靠。 变量的作用域 提到PHP的超

    • 这是问题陈述 一位印度农民有一片农田,比如说1平方公里长,他想种小麦或水稻,或者两者兼有。农民有有限的F公斤肥料和P公斤杀虫剂。 每平方公里的小麦需要F1公斤化肥和P1公斤杀虫剂。每平方公里的水稻种植需要F2公斤化肥和P2公斤杀虫剂。假设S1是出售从一平方公里收获的小麦获得的价格,S2是出售从一平方公里收获的水稻获得的价格。 你必须通过选择种植小麦和/或水稻的地区来找到农民可以获得的最大总利润。

    • 我想要一些关于如何实现以下目标的建议。我不提供代码,因为我的问题是理论上的,但应要求我可以。所以情况是这样的: 我有多个控制器,每个控制器都可以抛出XYException 我有一个@ControllerAdvice类,其中有一个@ExceptionHandler监视XYExceptions。当它发生时,它打印出“xy”。 在一个(且仅有一个)控制器中,当抛出XYException时,我希望执行一些

    • 问题内容: 抱歉,我是一个初学者,我无法确定这是一个多么好的问题,也许对你们中的某些人来说听起来很明显。 如果 我们 下面 使用 的这两个相同,哪个更好? 要么 通过使用,我的意思是我知道,在第二种情况下,我们还可以更改全局变量的值。但是如果我们不需要这样做,那是编写此函数的更好方法吗?传递变量比在函数中声明全局变量占用的内存少吗? 问题答案: 内存使用情况很少。更重要的是,代码易于遵循且不会产生

    • 我刚开始和拉威尔一起工作。我需要重写我几年前制作的整个系统,使用Laravel4作为基本框架。在我的旧系统中,我曾经有一个文件,其中声明了一些常量和一个文件,其中包含大量数组集(例如,类别、状态、事件类型、语言等)。通过这样做,我可以使用 在我的应用程序的任何地方。 我的问题是,我如何以所谓的“laravel方式”存储这些信息。我尝试使用某种对象来存储这些信息,将其设置为服务并为其创建门面: Ap