首页 > 解决方案 > 应用程序的运行地址,后跟堆和栈扩展

问题描述

我有一个m.c

extern void a(char*);

int main(int ac, char **av){
    static char string [] = "Hello , world!\n";
    a(string);
}

和一个a.c

#include <unistd.h>
#include <string.h>

void a(char* s){
    write(1, s, strlen(s));
}

我将这些编译并构建为:

g++ -c -g -std=c++14 -MMD -MP -MF "m.o.d" -o m.o m.c
g++ -c -g -std=c++14 -MMD -MP -MF "a.o.d" -o a.o a.c
g++ -o linux m.o a.o -lm -lpthread -ldl

然后,我检查可执行文件,linux因此:

objdump -drwxCS -Mintel linux

我的这个输出Ubuntu 16.04.6开始于:

start address 0x0000000000400540

然后,稍后是以下init部分:

00000000004004c8 <_init>:
  4004c8:   48 83 ec 08             sub    rsp,0x8

最后是fini部分:

0000000000400704 <_fini>:
  400704:   48 83 ec 08             sub    rsp,0x8
  400708:   48 83 c4 08             add    rsp,0x8
  40070c:   c3                      ret 

该程序引用通过命令获得的部分Hello , world!\n中的字符串:.data

objdump -sj .data linux

Contents of section .data:
 601030 00000000 00000000 00000000 00000000  ................
 601040 48656c6c 6f202c20 776f726c 64210a00  Hello , world!..

所有这一切都告诉我,可执行文件已被创建,以便从大约0x0000000000400540(地址.init)开始加载到实际内存地址中,并且程序访问实际内存地址中的数据,直到至少601040(地址.data

我基于John R Levine 的“Linkers & Loaders”的第 7 章,他在其中指出:

链接器将一组输入文件组合成一个输出文件,准备好在特定地址加载。

我的问题是关于下一行。

如果在加载程序时,该地址的存储不可用,则加载程序必须重新定位加载的程序以反映实际的加载地址。

(1)假设我有另一个可执行文件当前正在我的机器上运行,并且已经使用了400540和之间的内存空间601040,它是如何决定从哪里开始我的新可执行文件的linux

(2) 与此相关,第 4 章规定:

..ELF 对象...被加载在地址空间的中间,因此堆栈可以在文本段下方向下增长,而堆可以从数据末尾开始增长,从而使使用的总地址空间保持相对紧凑。

假设以前运行的应用程序开始于,比如说,200000现在linux开始于400540. 内存地址没有冲突或重叠。但是随着程序的继续,假设前一个应用程序的堆上升到300000,而新启动的堆linux下降到310000。很快,内存地址就会发生冲突/重叠。当冲突最终发生时会发生什么?

标签: clinuxassemblymemorylinker

解决方案


如果在加载程序时,该地址的存储不可用,则加载程序必须重新定位加载的程序以反映实际的加载地址。

并非所有文件格式都支持这一点:

32 位 Windows 的 GCC 将在动态库 ( .dll) 的情况下添加加载程序所需的信息。但是,该信息不会添加到可执行文件 ( .exe) 中,因此必须将此类可执行文件加载到固定地址。

在 Linux 下它有点复杂。但是,也不能将许多(通常是较旧的 32 位)可执行文件加载到不同的地址,而动态库 ( .so) 可以加载到不同的地址。

假设我有另一个可执行文件当前正在我的机器上运行,并且已经使用了400540601040...之间的内存空间。

现代计算机(所有 x86 32 位计算机)都有一个分页 MMU,大多数现代操作系统都使用它。这是一些电路(通常在 CPU 中),它将软件看到的地址转换为 RAM 看到的地址。在您的示例中,400540可以转换为1234000,因此访问地址400540实际上将访问1234000RAM 中的地址。

关键是:现代操作系统为不同的任务使用不同的 MMU 配置。因此,如果您再次启动程序,将使用不同的 MMU 配置,400540将软件看到的地址转换2345000为 RAM 中的地址。使用地址400540的两个程序可以同时运行,因为一个程序将实际访问地址1234000,而另一个程序将2345000在程序访问地址时访问 RAM 中的地址400540

这意味着400540在加载可执行文件时,某些地址(例如 )永远不会“已在使用中”。

加载动态库 ( .so/ )时,该地址可能已在使用中,因为这些库与可执行文件共享内存。.dll

...它是如何决定从哪里开始我的新可执行 linux 的?

在 Linux 下,如果可执行文件以无法移动到另一个地址的方式链接,则可执行文件将被加载到固定地址。(如前所述:这对于较旧的 32 位文件很典型。)在您的示例中,0x601040 如果您的编译器和链接器以这种方式创建可执行文件,则“Hello world”字符串将位于地址。

但是,大多数 64 位可执行文件可以加载到不同的地址。由于安全原因, Linux 会将它们加载到某个随机地址,从而使病毒或其他恶意软件更难以攻击程序。

...因此堆栈可以在文本段下方增长...

我从未在任何操作系统中看到过这种内存布局:

在 Linux 和 Solaris 下,堆栈都位于地址空间的末尾(大约在 附近0xBFFFFF00),而在非常接近内存开头的地方加载文本段(可能是 address 0x401000)。

...并且堆可以从数据的末尾长大,...

假设前一个应用程序的堆逐渐增加......

自 1990 年代后期以来的许多实现不再使用堆。相反,它们用于mmap()保留新内存。

根据手册页brk(),堆在 2001 年被声明为“遗留功能”,因此不应再被新程序使用。

(然而,根据 Peter Cordes的说法,malloc()在某些情况下似乎仍然使用堆。)

与 MS-DOS 等“简单”操作系统不同,Linux 不允许您“简单”地使用堆,但您必须调用该函数brk()来告诉 Linux 您要使用多少堆。

如果程序使用堆并且它使用的堆多于可用堆,则该brk()函数返回一些错误代码并且该malloc()函数简单地返回NULL.

但是,这种情况的发生通常是因为没有更多的 RAM 可用,而不是因为堆与其他一些内存区域重叠。

...而新推出的 linux 的堆栈已经向下增长到 ...

很快,内存地址就会发生冲突/重叠。当冲突最终发生时会发生什么?

实际上,堆栈的大小是有限的。

如果使用太多堆栈,则会出现“堆栈溢出”。

该程序将故意使用过多的堆栈 - 只是为了看看会发生什么:

.globl _start
_start:
    sub $0x100000, %rsp
    push %rax
    push %rax
    jmp _start

对于带有 MMU 的操作系统(例如 Linux),您的程序将崩溃并显示错误消息:

~$ ./example_program
Segmentation fault (core dumped)
~$

编辑/附录

所有正在运行的程序的堆栈是否位于“末尾”?

在较旧的 Linux 版本中,堆栈位于程序可访问的虚拟内存的末尾附近(但不完全位于) :在这些 Linux 版本中,程序可以访问地址范围从0到。0xBFFFFFFF初始堆栈指针位于0xBFFFFE00. (命令行参数和环境变量在堆栈之后。)

这是实际物理内存的终结吗?不同运行程序的堆栈不会混在一起吗?我的印象是程序的所有堆栈和内存在实际物理内存中保持连续,...

在使用 MMU 的计算机上,程序永远不会看到物理内存:

加载程序时,操作系统将搜索 RAM 的一些空闲区域 - 也许它会在物理地址找到一些0xABC000。然后它以将虚拟地址0xBFFFF000-0xBFFFFFFF转换为物理地址的方式配置 MMU 0xABC000-0xABCFFF

这意味着:每当程序访问地址0xBFFFFE20(例如使用push操作)时,0xABCE20实际访问的是 RAM 中的物理地址。

程序根本不可能访问某个物理地址。

如果您有另一个程序正在运行,则 MMU 的配置方式是在其他程序运行时将地址0xBFFFF000-0xBFFFFFFF转换为地址0x345000-0x345FFF

因此,如果两个程序之一将执行push操作并且堆栈指针为0xBFFFFE20,则将访问 RAM 中的地址0xABCE20;如果另一个程序执行一个push操作(使用相同的堆栈指针值),则该地址0x345E20将被访问。

因此,堆栈不会混淆。

不使用 MMU 但支持多任务的操作系统(例如 Amiga 500 或早期的 Apple Macintoshes)当然不会以这种方式工作。此类操作系统使用特殊的文件格式(而不是 ELF),这些格式针对在没有 MMU 的情况下运行多个程序进行了优化。为此类操作系统编译程序比为 Linux 或 Windows 编译程序复杂得多。甚至对软件开发人员也有限制(例如:函数和数组不应该太长)。

另外,每个程序是否都有自己的堆栈指针、基指针、寄存器等?或者操作系统是否只有一组这些寄存器供所有程序共享?

(假设是单核CPU),CPU有一组寄存器;并且只能同时运行一个程序。

当您启动多个程序时,操作系统将在程序之间切换。这意味着程序 A 运行(例如)1/50 秒,然后程序 B 运行 1/50 秒,然后程序 A 运行 1/50 秒,依此类推。在您看来,这些程序似乎在同一时间运行。

当操作系统从程序 A 切换到程序 B 时,它必须首先保存(程序 A)寄存器的值。然后它必须更改 MMU 配置。最后它必须恢复程序 B 的寄存器值。


推荐阅读