开始和结束进程段的获取 C/C++

5

我需要获取以下过程段的起始和结束地址:代码、数据、堆栈、环境。我知道它们在内存中的位置,但不知道如何使用API调用或其他方式来获取它们。使用以下代码已经找到了一些段的起始地址:

#include <stdio.h>

int temp_data = 100;
static int temp_bss;

void print_addr ( void )
{
        int local_var = 100;
        int *code_segment_address = ( int* ) &print_addr;
        int *data_segment_address = &temp_data;
        int *bss_address = &temp_bss;
        int *stack_segment_address = &local_var;

        printf ( "\nAddress of various segments:" );
        printf ( "\n\tCode Segment : %p" , code_segment_address );
        printf ( "\n\tData Segment : %p" , data_segment_address );
        printf ( "\n\tBSS : %p" , bss_address );
        printf ( "\n\tStack Segment : %p\n" , stack_segment_address );

}

int main ( )
{
        print_addr ();
        return 0;
}

但我不知道如何找到每个段落的结尾。我唯一的想法是一个段落的结尾就是另一个段落的开头。 请说明如何使用C和Linux API实现。


1
你想从内存中的进程获取这个信息?还是从磁盘上的 ELF 可执行文件中获取?无论哪种方式,信息都在 ELF 头部中。你应该查看 ELF 规范。 - Jonathon Reinhart
你真的应该解释一下你的动机,并编辑你的问题,解释为什么你需要所有这些! - Basile Starynkevitch
1
我很好奇,你为什么需要得到这些东西? - Oliver Charlesworth
1
你计算代码段开头的代码是不正确的。很难预测你的代码以什么顺序出现在可执行文件中,但链接器通常会在第一个函数之前插入一个PLT表,因此仅通过获取函数的地址永远无法获得代码段的开头。 - fuz
1
同样的情况也适用于你的其他代码。你的 print_addr() 函数不会是第一个堆栈帧,而且在 temp_data 之前的数据段中会有其他变量。请考虑查看 objdump 的输出以了解你的可执行文件的外观。 - fuz
显示剩余2条评论
4个回答

7
我不确定数据或堆段是否定义明确并且唯一(特别是在多线程应用程序中,或者仅使用动态库(包括 libc.so)的应用程序中)。换句话说,在今天的进程中,没有任何文本、数据或堆段的明确定义的起始和结束,因为一个进程有许多这样的段。因此,在一般情况下,你的问题甚至没有意义。
大多数malloc实现比sbrk更多地使用mmap(2)munmap

您应该阅读更多关于proc(5)的内容。特别是,您的应用程序可以读取/proc/self/maps(或/proc/1234/maps以获取pid为1234的进程)或/proc/self/smaps;尝试cat /proc/self/maps并考虑在"/proc/self/maps"上使用fopen(3)(然后在fgetsreadline上循环,最后快速地使用fclose)。也许dladdr(3)可能相关。

你还可以阅读程序的ELF头文件,例如/proc/self/exe。另请参阅readelf(1)objdump(1)execve(2)&elf(5)&ld.so(8)&libelf。同时阅读Levine的《链接器和装载器》一书和Drepper的论文《如何编写共享库》

另请参阅此答案,了解相关问题(以及该问题)。请注意,最近的Linux系统具有ASLR,因此在相同环境中运行相同程序的两个类似进程的地址布局将不同。

还可以尝试对某些简单命令或您的程序进行strace(1)。您将更好地了解相关syscalls(2)。还要阅读Advanced Linux Programming


谢谢您的回答,您能否举个例子来说明如何使用proc获取地址。 - user3991417
3
现在已经不存在一个单一的代码段了。在终端中运行 cat /proc/self/maps 并尝试理解它的输出。 - Basile Starynkevitch
1
哦,我对此感到非常失望,我是新手。您能否提供一个从这些文件中获取任何段地址的示例?提前感谢。 - user3991417
4
现代Linux系统上已经不存在文本段和数据段。文本被分布在多个共享库中,而数据则从多个mmap()调用中散布在地址空间中。你所需要的东西已经不存在了。 - fuz
2
@ketazafor:所有开始需要的相关信息都在这个答案中。具体细节需要你去发现和理解。 - pts
显示剩余4条评论

4

查看man 3 end获取一些帮助:

#include <stdio.h>
extern etext;
extern edata;
extern end;
int
main(int ac, char **av, char **env)
{
        printf("main %p\n", main);
        printf("etext %p\n", &etext);
        printf("edata %p\n", &edata);
        printf("end %p\n", &end);
        return 0;
}

这3个符号的地址是文本、初始化数据和未初始化数据段结束后的第一个地址。
您可以通过main()的第三个参数访问环境变量,就像上面的示例代码一样,但您也可以从地址&argv[0]开始向上遍历堆栈。最后一个指向命令行参数字符串的指针之后有一个空值单词(32位或64位,取决于CPU)。在那个NULL之后是环境变量。
堆栈顶部几乎无法以编程方式获取 - 现代操作系统都执行“地址空间布局随机化”(ASLR)以提供缓解缓冲区溢出的措施。堆栈的“末尾”很模糊,因为您可以在堆栈上分配(通过递归或alloca()),直到遇到堆的顶部。因此,“堆栈”的“末尾”取决于所涉及程序的分配模式。
您还应该了解ELF辅助向量。请参见man getauxval以获取C语言接口,并参见此文章以获取一些解释。用户程序永远不会使用ELF辅助向量,但它与动态链接紧密相关。

这对于当前的Linux系统、动态链接的可执行文件和多线程进程来说并不是非常相关。它更多地与上个世纪有关。 - Basile Starynkevitch
请注意,访问环境的可移植方式是通过全局变量 environ - fuz

1
如另一个评论所说,文本、数据和堆栈段的概念在Linux上并不存在。程序文本分布在共享库中,并且内存分配使用mmap()而不是brk(),导致分配的数据散布在程序地址空间中。
尽管如此,您可以使用brk()系统调用来找到数据段的末尾,您可以使用符号etextedataend来找到可执行文件的边界。文本段的开始通常是固定的(也称为“加载地址”),并取决于架构和链接器配置。请注意,您的程序很可能会在二进制文件的文本部分之外执行代码,并且很可能不会使用brk分配任何动态内存。
有关更多详细信息,请参见相应的手册页面。

0

目前的Windows和Linux版本使用平面地址空间,这意味着代码和数据段是相同的,并且几乎总是从0到2^32-1(32位系统)和2^64-1(64位系统)。不同的进程通常具有完全不同的地址空间,除了共享内存。通常只有一些地址空间的部分被映射到内存中,而由于硬件限制,有些部分甚至无法寻址。

链接器的代码和数据段成为可运行图像的“节”,在Linux下常见的ELF格式会增加一些额外的复杂性。访问高度依赖于操作系统,因此并不是C++问题。

在Windows下,您可以通过GetModuleHandle(0)获取指向加载图像开头的指针。通过遍历可执行文件头,您可以找到COFF节表,从而将所有属于映射可执行图像的地址反向映射到它们各自的节。分类其他地址更加困难;它们可能属于其他映射的运行图像(已加载的DLL),或者它们可能属于以其他方式分配的地址范围,即直接通过VirtualAlloc()或间接地通过某种方式分配(HeapAlloc(),内存映射文件等)。

如果你只想打印漂亮的堆栈跟踪或其他内容,那么有很多现成的库可以为你完成。如果你想要做校验和,那么事情会变得更加复杂;最好使用代码签名或现成的库。对于你的问题的真正答案取决于你实际遇到的问题是什么...


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接