`const char *` 在库 .so 文件中存储的奇怪行为

Posted

技术标签:

【中文标题】`const char *` 在库 .so 文件中存储的奇怪行为【英文标题】:Strange behaviour of `const char *` storage in libraries .so files 【发布时间】:2016-11-16 11:12:52 【问题描述】:

我有一个在 android 中使用的库,但我很确定该问题并非特定于 Android。 这个库包含一堆我打印到 logcat 的错误代码,它们都由一个常量字符串组成。

...
if(...)ALOGE("Error in parameter XXXXXX");
if(...)ALOGE("Error in parameter YYYYYY");
if(...)ALOGE("Error in parameter ZZZZZZ");
...

今天我注意到我的 .rodata 部分中有大量数据(大约 16kB)。所以我运行了strings mylib.so,得到了一堆字符串。

Error in parameter XXXXXX
Error in parameter YYYYYY
Error in parameter ZZZZZZ

不过,我只需要一点额外的打印成本(应该没问题,因为这些代码很少使用),如果我将字符串分成两部分,我可以节省很多空间。然后编译器应该完成这项工作并将公共部分组合在一个字符串中。由于编译器有一个重复的字符串删除优化步骤(CLANG 和 GCC)。

我是这样做的:(我有很多,但它们都有这样的模式,我知道我应该使用定义(但这是一个快速测试))

...
if(...)ALOGE("Error in parameter %s","XXXXXX");
if(...)ALOGE("Error in parameter %s","YYYYYY");
if(...)ALOGE("Error in parameter %s","ZZZZZZ");
...

我发现的是:

    库的大小完全相同。 .rodata 现在要小得多,但 .text 增加了几乎相同的数量。 (仅几个字节的差异) strings 命令现在只打印 1 次 "Error in parameter %s" 字符串和分隔的部分。所以没有发生字符串合并。 如果我以 32 位、64 位等方式编译似乎并不重要。

那么,这里发生了什么?我该如何解决?有什么指导吗?编译器在做什么? 谢谢

额外数据:

编译器 CLANG 4.9(4.8 的结果相同)。 标志:-Os -fexceptions -std=c++11 -fvisivility=hidden

编辑:

我使用 GCC 创建了一个在线示例测试,结果相同 Online GCC

拆分:

#include <stdio.h>
int main()

    int a = rand()%7;
    switch(a)
        case 0: printf("Hello, %s!\n","Anna"); break;
        case 1: printf("Hello, %s!\n","Bob"); break;
        case 2: printf("Hello, %s!\n","Clark"); break;
        case 3: printf("Hello, %s!\n","Danniel"); break;
        case 4: printf("Hello, %s!\n","Edison"); break;
        case 5: printf("Hello, %s!\n","Foo"); break;
        case 6: printf("Hello, %s!\n","Garret"); break;
    
    return 0;

非拆分:

#include <stdio.h>
int main()

    int a = rand()%7;
    switch(a)
        case 0: printf("Hello, Anna!\n"); break;
        case 1: printf("Hello, Bob!\n"); break;
        case 2: printf("Hello, Clark!\n"); break;
        case 3: printf("Hello, Danniel!\n"); break;
        case 4: printf("Hello, Edison!\n"); break;
        case 5: printf("Hello, Foo!\n"); break;
        case 6: printf("Hello, Garret!\n"); break;
    
    return 0;

编译:

gcc -Os -o main main.c
gcc -Os -o main2 main2.c

尺寸:

-rwxr-xr-x 1 20446 20446 8560 Nov 16 11:43 main                     
-rw-r--r-- 1 20446 20446  478 Nov 16 11:41 main.c 
-rwxr-xr-x 1 20446 20446 8560 Nov 16 11:42 main2   
-rw-r--r-- 1 20446 20446  443 Nov 16 11:39 main2.c

字符串:

    strings main2 | grep "Hello"                                  
Hello, Anna!                                                          
Hello, Bob!                                                          
Hello, Clark!                                                         
Hello, Danniel!                                                       
Hello, Edison!                                                       
Hello, Foo!                                                         
Hello, Garret!

    strings main | grep "Hello"                                  
Hello, %s!                                                          

【问题讨论】:

.text 用于代码 你用的是什么编译器?这是 C 代码还是 C++ 代码?您正在使用哪些优化设置? 它是 C++,标志并没有什么特别之处。只是-Os。如果我没记错的话,编译器是 clang 4.9 (ndk r13b) 如果您没有设置 -fwritable-strings 标志,GCC 应该始终启用字符串池。确保 clang 默认启用了字符串池。 我使用 GCC 和在线工具,得到了相同的结果。我会调查你告诉我的旗帜。 【参考方案1】:

您的所有期望都相当正确,但测试用例不足以证明效果。首先,二进制可执行文件具有“段/节对齐”的概念(或类似的东西)。简而言之,这意味着不同部分的第一个字节只能放置在某个值的倍数的文件偏移处(例如十进制512)。部分之间未使用的空间用零填充以满足此要求。并且您的测试用例提供的所有数据都不会耗尽该填充,因此您感觉不到真正的差异。接下来 - 如果你想更清楚地比较效果 - 你不应该链接到启动代码,即你应该用最少的引用而不是常规的可执行文件来构建动态库。

接下来,我的测试程序。它和你的有点不同。但在概念上并非如此。

#include <stdio.h>

#if defined(_SPLIT)
#define LOG(str) printf("Very very very loooo-o-o-o-o-o-o-ooooong prefix %s", str )
#elif defined(_NO_SPLIT)
#define LOG(str) printf("Very very very loooo-o-o-o-o-o-o-ooooong prefix " str )
#else
#error "Don't know what you want."
#endif

int foo(void) 
    LOG("aaaaaaaa");
    LOG("bbbbbbbb");
    LOG("cccccccc");
    LOG("dddddddd");
    LOG("eeeeeeee");
    LOG("ffffffff");
    LOG("gggggggg");
    LOG("hhhhhhhh");
    LOG("iiiiiiii");
    LOG("jjjjjjjj");
    LOG("kkkkkkkk");
    LOG("llllllll");
    LOG("mmmmmmmm");
    LOG("nnnnnnnn");
    LOG("oooooooo");
    LOG("pppppppp");
    LOG("qqqqqqqq");
    LOG("rrrrrrrr");
    LOG("ssssssss");
    LOG("tttttttt");
    LOG("uuuuuuuu");
    LOG("vvvvvvvv");
    LOG("wwwwwwww");
    LOG("xxxxxxxx");
    LOG("yyyyyyyy");
    LOG("zzzzzzzz");
    return 0;

然后,让我们创建动态库:

$ gcc --shared -fPIC -o t_no_split.so -D_NO_SPLIT test.c
$ gcc --shared -fPIC -o t_split.so -D_SPLIT test.c

并比较尺寸:

-rwxr-xr-x  1 sysuser sysuser   12098 Nov 16 14:19 t_no_split.so
-rwxr-xr-x  1 sysuser sysuser    8002 Nov 16 14:19 t_split.so

IMO,确实存在显着差异。而且,老实说,我没有检查每个部分的大小,但无论如何你可以自己做。

当然,这并不意味着未拆分的字符串使用12098 - 8002 字节多于拆分的字符串。这只是意味着编译器/链接器必须为t_no_split.so 使用比t_split.so 更多的空间。而这种膨胀肯定是由字符串大小的差异引起的。另一个有趣的事情 - 拆分甚至抵消了由于将第二个参数传递给 printf() 而导致的机器代码的小膨胀。

附:我的机器是 x64 Linux,GCC 4.8.4。

【讨论】:

哦,谢谢你,我明白了!显然,案例中有一个神奇的 4k 大小膨胀(确切地说是 4096),所以也许这就是原因。如果您没有设法将代码大小减少一整页,编译器仍会在.rodata 中分配页面,并以 0 提交,即使该页面仅包含一个字节。所以我要么进一步减少我的字符串,要么什么都不做。明天我将检查正确的代码以查看 .rodata 的页数。 确实,我在.rodata 中使用了 0x66f8。通过我的优化,0x60f0。但由于页面大小为 0x1000,我根本没有看到大小下降。但很高兴知道这些优化是可能的! @DarkZeros 尽管如此 - 它可以工作,并且可能在未来与其他一些技巧或优化一起使用。【参考方案2】:

您只为每个字符串节省了 19 个字节,但代价是向看起来像 varargs 函数的东西传递了一个额外的参数。至少是加载地址和推送。

让我猜猜,ALOGE实际上是一个宏?

我认为您不需要 DEFINE - 您需要一个函数(不是内联),例如:

void BadParameter(const char * paramName)

    ALOGE("Error in parameter %s", paramName);

...并用它替换所有调用。

【讨论】:

我明白,但是我的真实代码有更长的字符串,我计算了一下我应该保存大约 1kB 的字符串,奇怪的是大小完全相同。我做了一个在线示例,结果相同。 请记住,可执行文件的大小很可能是整数页数。我不知道在这种情况下“页面”有多大,但 4k 是很有可能的。

以上是关于`const char *` 在库 .so 文件中存储的奇怪行为的主要内容,如果未能解决你的问题,请参考以下文章

在 Swift 中使用 const char * 初始化 C-struct

使用 ctypes 在 Python 中解码 C const char*

const char []和const char之间的区别*

使用 CMake 在库中查找符号

初始化静态 const char* 数组

深入理解const char*p,char const*p,char *const p,const char **p,char const**p,char *const*p,char**const p