如何在运行时确定共享库中全局变量的地址范围?

Posted

技术标签:

【中文标题】如何在运行时确定共享库中全局变量的地址范围?【英文标题】:How to determine the address range of global variables in a shared library at runtime? 【发布时间】:2019-10-05 11:33:17 【问题描述】:

在运行时,加载的共享库中的全局变量是否保证占用连续的内存区域?如果有,是否有可能找出那个地址范围?

上下文:我们希望在内存中拥有共享库的多个“实例”(例如协议栈实现)以用于模拟目的(例如模拟具有多个主机/路由器的网络)。我们正在尝试的方法之一是只加载一次库,但通过创建和维护全局变量的“影子”集来模拟其他实例,并通过memcpy()'ing 将适当的影子集输入/输出来在实例之间切换库的全局变量占用的内存区域。 (使用dlmopen() 多次加载库或在共享库中引入间接访问全局变量等替代方法也有其局限性和困难。)

我们尝试过的事情:

使用dl_iterate_phdr()查找共享库的数据段。结果地址范围并不太有用,因为(1)它没有指向包含实际全局变量的区域,而是指向从 ELF 文件(在只读内存中)加载的段,并且(2)它不仅包含全局变量,还有其他内部数据结构。

将 C 中的开始/结束保护变量添加到库代码中,并确保(通过链接器脚本)将它们放置在共享对象中 .data 部分的开始和结束处。 (我们用objdump -t 验证了这一点。)这个想法是,在运行时,所有全局变量都将位于两个保护变量之间的地址范围内。然而,我们的观察是内存中实际变量的相对顺序与共享对象中的地址所遵循的顺序完全不同。一个典型的输出是:

$ objdump -t libx.so | grep '\.data'
0000000000601020 l    d  .data  0000000000000000              .data
0000000000601020 l     O .data  0000000000000000              __dso_handle
0000000000601038 l     O .data  0000000000000000              __TMC_END__
0000000000601030 g     O .data  0000000000000004              custom_data_end_marker
0000000000601028 g     O .data  0000000000000004              custom_data_begin_marker
0000000000601034 g       .data  0000000000000000              _edata
000000000060102c g     O .data  0000000000000004              global_var

$ ./prog
# output from dl_iterate_phdr()
name=./libx.so (7 segments)
    header  0: type=1 flags=5 start=0x7fab69fb0000 end=0x7fab69fb07ac size=1964
    header  1: type=1 flags=6 start=0x7fab6a1b0e08 end=0x7fab6a1b1038 size=560  <--- data segment
    header  2: type=2 flags=6 start=0x7fab6a1b0e18 end=0x7fab6a1b0fd8 size=448
    header  3: type=4 flags=4 start=0x7fab69fb01c8 end=0x7fab69fb01ec size=36
    header  4: type=1685382480 flags=4 start=0x7fab69fb0708 end=0x7fab69fb072c size=36
    header  5: type=1685382481 flags=6 start=0x7fab69bb0000 end=0x7fab69bb0000 size=0
    header  6: type=1685382482 flags=4 start=0x7fab6a1b0e08 end=0x7fab6a1b1000 size=504

# addresses obtained via dlsym() are consistent with the objdump output:
dlsym('custom_data_begin_marker') = 0x7fab6a1b1028
dlsym('custom_data_end_marker') =   0x7fab6a1b1030  <-- between the begin and end markers

# actual addresses: at completely different address range, AND in completely different order!
&custom_data_begin_marker = 0x55d613f8e018
&custom_data_end_marker =   0x55d613f8e010  <-- end marker precedes begin marker!
&global_var =               0x55d613f8e01c  <-- after both markers!

这意味着“保护变量”方法不起作用。

也许我们应该遍历全局偏移表 (GOT) 并从那里收集全局变量的地址?但是,如果可能的话,似乎没有官方的方法可以做到这一点。

有什么我们忽略的吗?如果需要,我很乐意澄清或发布我们的测试代码。

编辑:澄清一下,有问题的共享库是第 3 方库,我们不想修改其源代码,因此寻求上述通用解决方案。

EDIT2:作为进一步说明,以下代码概述了我希望能够做的事情:

// x.c -- source for the shared library
#include <stdio.h>

int global_var = 10;

void bar() 
    global_var++;
    printf("global_var=%d\n", global_var);

// a.c -- main program
#include <stdlib.h>
#include <dlfcn.h>
#include <memory.h>

struct memrange 
    void *ptr;
    size_t size;
;

extern int global_var;
void bar();

struct memrange query_globals_address_range(const char *so_file)

    struct memrange result;
    // TODO what generic solution can we use here instead of the next two specific lines?
    result.ptr = &global_var;
    result.size = sizeof(int);
    return result;


struct memrange g_range;


void *allocGlobals()

    // allocate shadow set and initialize it with actual global vars
    void *globals = malloc(g_range.size);
    memcpy(globals, g_range.ptr, g_range.size);
    return globals;


void callBar(void *globals) 
    memcpy(g_range.ptr, globals, g_range.size); // overwrite globals from shadow set
    bar();
    memcpy(globals, g_range.ptr, g_range.size);  // save changes into shadow set


int main(int argc, char *argv[])

    g_range = query_globals_address_range("./libx.so");

    // allocate two shadow sets of global vars
    void *globals1 = allocGlobals();
    void *globals2 = allocGlobals();

    // call bar() in the library with a few times with each
    callBar(globals1);
    callBar(globals2);
    callBar(globals2);
    callBar(globals1);
    callBar(globals1);

    return 0;

构建+运行脚本:

#! /bin/sh
gcc -c -g -fPIC x.c -shared -o libx.so
gcc a.c -g -L. -lx -ldl -o prog
LD_LIBRARY_PATH=. ./prog

EDIT3:添加了dl_iterate_phdr() 输出

【问题讨论】:

请务必查看Address space layout randomization 【参考方案1】:

共享库编译为Position-Independent Code。这意味着与可执行文件不同,地址不是固定的,而是在动态链接期间确定的。

从软件工程的角度来看,最好的方法是使用对象(结构)来表示所有数据并避免使用全局变量(这种数据结构通常称为“上下文”)。然后,所有 API 函数都接受一个上下文参数,这允许您在同一进程中拥有多个上下文。

【讨论】:

谢谢,但您的回复没有回答原始问题。使用上下文对象的建议是有效的,它与问题中提到的替代方案之一基本相同(“在共享库中引入间接访问全局变量”)。澄清一下,我们选择不这样做,因为它需要对库源代码进行大量更改,而且所讨论的库是我们希望尽可能保持原样的第三方软件。 我的主要观点是,如果您有多个全局变量实例,您还需要多个函数实例(由于 PIC)。 其实我只使用了一个全局变量的实例。我想通过拥有全局变量的单独影子/备份副本来“模拟”多个实例,并使用memcpy 进行切换。 IE。在调用库函数之前,我从适当的影子集中覆盖库的 var,在调用之后,我通过从实际 var 覆盖影子集来保存库所做的修改。我将在问题中添加代码以澄清这一点。【参考方案2】:

在运行时,加载的共享库中的全局变量是否保证占用连续的内存区域?

是的:在任何ELF 平台(例如Linux)上,所有可写全局变量通常都被分组到一个可写PT_LOAD 段中,并且该段位于固定地址(在库加载时确定)。

如果是的话,是否有可能找出那个地址范围?

当然。您可以使用dl_iterate_phdr 找到库加载地址,并遍历它提供给您的程序段。程序头之一将具有.p_type == PT_LOAD.p_flags == PF_R|PF_W。你要的地址范围是[dlpi_addr + phdr-&gt;p_vaddr, dlpi_addr + phdr-&gt;p_vaddr + phdr-&gt;p_memsz)

这里:

# actual addresses: completely different order:

您实际上是在查看主可执行文件中GOT 条目的地址,而不是变量本身的地址。

【讨论】:

我确实尝试了dl_iterate_phdr() 并找到了带有type=1 (PT_LOAD)flags=6 (R+W) 的段,并在你写的时候提取了地址范围。但是,正如问题中提到的那样,事实证明该地址范围指向加载的 ELF 标头,该标头实际上包含全局变量和一些内部内容的初始化值,并且是只读的,因为由加载相同的所有进程共享库。它与我感兴趣的无关:共享库的全局变量在当前进程中的位置。 “它与我感兴趣的东西没有关系”——你错了:有一个PT_LOADR+W封面所有全局变量(来自给定的共享库)。请注意,PT_LOADR+W 可能不止一个,具体取决于使用的链接器以及-Wl,--rosegment 是否有效。 我希望你是对的。我将dl_iterate_phdr() 输出添加到问题文本中——遗憾的是,它只为我系统上的共享库显示了一个PT_LOADR+W(Ubuntu 18.04 LTS、GNU ld 2.30、ld-2.27.so)。 dlsym() 返回指向该区域的地址,但实际变量位于不同的地址范围内(由&amp;global_var 获得,并使用%p 打印)。我有兴趣以通用方式查询后者,但我开始认为没有办法做到这一点。 “我希望你是对的。” -- 我是对的,你的输出清楚地表明了这一点。 “dlsym() 返回指向该区域的地址”——宾果游戏! “但实际变量在不同的地址范围内”——这就是 you 感到困惑的地方。这些是GOT 条目的地址,不是实际变量的地址!哦,我明白了。您链接程序的方式强制复制重定位。我会更新答案。 我想知道&amp;foo(如果foo 是共享库中的int 全局变量)可能是GOT 条目的地址。您的意思是在执行int *ptr=&amp;foo; 语句后,ptr 将指向 GOT 条目?但是根据 C 规则,foo=3; 赋值相当于*(&amp;foo)=3;,相当于*ptr=3;——应该(尝试)覆盖 GOT 条目?这是一个矛盾。 &amp;foo 必须是存储变量 value 的地址,无论该地址的计算方式如何(是否通过索引 GOT)。所以,还在寻找解决方案……

以上是关于如何在运行时确定共享库中全局变量的地址范围?的主要内容,如果未能解决你的问题,请参考以下文章

Solaris 共享库和全局变量

C++ 静态库中的共享全局变量:Linux

如何通过多进程共享(或排除共享)全局变量?

是共享库/dll中的全局变量,跨进程共享

如果使用调试信息编译,则通过其名称获取全局变量地址

不同的静态全局变量共享相同的内存地址