c - 应用程序的运行地址,后跟堆和栈扩展
问题描述
我有一个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
。很快,内存地址就会发生冲突/重叠。当冲突最终发生时会发生什么?
解决方案
如果在加载程序时,该地址的存储不可用,则加载程序必须重新定位加载的程序以反映实际的加载地址。
并非所有文件格式都支持这一点:
32 位 Windows 的 GCC 将在动态库 ( .dll
) 的情况下添加加载程序所需的信息。但是,该信息不会添加到可执行文件 ( .exe
) 中,因此必须将此类可执行文件加载到固定地址。
在 Linux 下它有点复杂。但是,也不能将许多(通常是较旧的 32 位)可执行文件加载到不同的地址,而动态库 ( .so
) 可以加载到不同的地址。
假设我有另一个可执行文件当前正在我的机器上运行,并且已经使用了
400540
和601040
...之间的内存空间。
现代计算机(所有 x86 32 位计算机)都有一个分页 MMU,大多数现代操作系统都使用它。这是一些电路(通常在 CPU 中),它将软件看到的地址转换为 RAM 看到的地址。在您的示例中,400540
可以转换为1234000
,因此访问地址400540
实际上将访问1234000
RAM 中的地址。
关键是:现代操作系统为不同的任务使用不同的 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 的寄存器值。
推荐阅读
- logstash - 安装 logstash-filter-rest 消息时显示错误:证书验证失败
- android - 迁移到 New Places SDK 客户端请求 Play 商店的 ACCESS_FINE_LOCATION 权限
- java - 类加载,静态块
- python - 在 Django 命令测试中使用两个不同的数据集修补外部 API 调用
- entity-framework-core - EF Core 可以跟踪非最高投影中的实体吗?
- apache-spark - 基于 Direct Stream 的 SparkStreaming 与 Kafka 仅显示一个 Consumer-ID
- r - R中带有tesseract的OCR无法识别所有换行符
- ruby-on-rails - 如果我可以在 redmine 控制器中的“before action”之后添加“if”
- java - Spring构建CSV字符串并用它下载一个文件
- jquery - 如何检查jquery验证器中每个验证修复中剩余多少无效字段