运行 应用程序地址,后跟堆和堆栈扩展
Running address of an application, followed by heap and stack expansions
我有一个 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
程序引用了.data
段中的字符串Hello , world!\n
,通过命令获得:
objdump -sj .data linux
Contents of section .data:
601030 00000000 00000000 00000000 00000000 ................
601040 48656c6c 6f202c20 776f726c 64210a00 Hello , world!..
所有这些告诉我,可执行文件已经创建,以便加载到从大约 0x0000000000400540
开始的实际内存地址(.init
的地址)并且程序访问实际内存中的数据地址至少扩展到 601040
(.data
的地址)
我基于 "Linkers & Loaders" by John R Levine 的第 7 章,他在其中指出:
A linker combines a set of input files into a single output file that
is ready to be loaded at a specific address.
我的问题是关于下一行的。
If, when the program is loaded, storage at that address isn't
available, the loader has to relocate the loaded program to reflect
the actual load address.
(1) 假设我有另一个可执行文件当前在我的机器上 运行 已经在使用 400540
和 601040
之间的内存 space,它是如何决定的从哪里开始我的新可执行文件 linux
?
(2) 与此相关,在第4章中,说:
..ELF objects...are loaded in about the middle of the address space so
the stack can grown down below the text segment and the heap can grow
up from the end of the data, keeping the total address space in use
relatively compact.
假设之前的 运行 应用程序开始于 200000
,现在 linux
开始于 400540
。内存地址没有冲突或重叠。但是随着程序的继续,假设前一个应用程序的堆上升到 300000
,而新启动的 linux
的堆已经下降到 310000
。很快,就会有一个clash/overlap的内存地址。当冲突最终发生时会发生什么?
是的,此可执行文件上的 objdump 显示了其段将被映射到的地址。 (链接将部分收集到段中:What's the difference of section and segment in ELF file format).data
和 .text
链接到具有不同权限的不同部分(读+写与读+执行)。
If, when the program is loaded, storage at that address isn't available
这只会在加载动态库时发生,而不是可执行文件本身。虚拟内存意味着每个进程都有自己的私有虚拟地址space,即使它们是从同一个可执行文件启动的。 (这也是为什么 ld
总是可以为 text
和 data
段选择相同的默认基地址,而不是试图将系统上的每个可执行文件和库放入一个不同的位置单个地址 space.)
当 OS 的 ELF 程序加载器 loaded/mapped 时,可执行文件是第一个声明该地址 space 部分的东西。这就是为什么传统的 (non-PIE) ELF 可执行文件可以是 non-relocatable,不像像 /lib/libc.so.6
这样的 ELF 共享对象
如果您 single-step 一个带有调试器的程序,或者包含一个睡眠程序,您将有时间查看 less /proc/<PID>/maps
。或者 cat /proc/self/maps
让猫给你看它自己的地图。 (还有 /proc/self/smaps
有关每个映射的更多详细信息,例如有多少是脏的,使用大页面等)
(较新的 GNU/Linux 发行版默认将 GCC 配置为生成 PIE 可执行文件:32-bit absolute addresses no longer allowed in x86-64 Linux?。在这种情况下,objdump 只会看到相对于 0
或 1000
之类的。而且 compiler-generated asm 会使用 PC-relative 寻址,而不是绝对寻址。)
If, when the program is loaded, storage at that address isn't available, the loader has to relocate the loaded program to reflect the actual load address.
并非所有文件格式都支持:
32 位 GCC Windows 将在动态库 (.dll
) 的情况下添加加载程序所需的信息。但是,这些信息并没有添加到可执行文件中(.exe
),所以这样的可执行文件必须加载到固定地址。
Linux下有点复杂;但是,也不可能将许多(通常是较旧的 32 位)可执行文件加载到不同的地址,而动态库 (.so
) 可以加载到不同的地址。
Suppose I have another executable that is currently running on my machine already using the memory space between 400540
and 601040
...
现代计算机(所有 x86 32 位计算机)都有一个分页 MMU,大多数现代操作系统都使用它。这是一些电路(通常在 CPU 中),它将软件看到的地址转换为 RAM 看到的地址。在您的示例中,400540
可以转换为 1234000
,因此访问地址 400540
实际上将访问 RAM 中的地址 1234000
。
重点是:现代 OS 对不同的任务使用不同的 MMU 配置。因此,如果您再次启动程序,将使用不同的 MMU 配置,将软件看到的地址 400540
转换为 RAM 中的地址地址 2345000
。使用地址 400540
的两个程序可以同时 运行 因为一个程序实际上访问地址 1234000
而另一个程序访问地址 2345000
地址 400540
.
这意味着加载可执行文件时某些地址(例如400540
)将永远不会“已在使用”。
加载动态库(.so
/.dll
)时该地址可能已被使用,因为这些库与可执行文件共享内存。
... how is it decided where to start my new executable linux?
在Linux下,如果可执行文件以无法移动到另一个地址的方式链接,则可执行文件将被加载到固定地址。 (如前所述:这对于较旧的 32 位文件来说很典型。)在您的示例中,“Hello world”字符串将位于地址 0x601040
if 您的编译器和链接器以这种方式创建了可执行文件。
但是,大多数 64 位可执行文件可以加载到不同的地址。 Linux 会将它们加载到一些 随机 地址,因为安全原因使病毒或其他恶意软件更难攻击程序。
... so the stack can grown down below the text segment ...
我从未在任何操作系统中见过这种内存布局:
在 Linux 和 Solaris 下,堆栈都位于地址 space 的末尾(在 0xBFFFFF00
附近),而文本段的加载非常接近内存的开始(可能是地址 0x401000
)。
... and the heap can grow up from the end of the data, ...
假设前一个应用程序的堆爬升...
自 1990 年代后期以来的许多实现不再使用堆。相反,他们使用 mmap()
来保留新内存。
根据 brk()
的手册页,堆在 2001 年被声明为“遗留功能”,因此新程序不应再使用它。
(但是,根据 Peter Cordes 的说法,malloc()
在某些情况下似乎仍然使用堆。)
不像MS-DOS这样的“简单”操作系统,Linux不允许您“简单地”使用堆,但您必须调用函数brk()
来告诉Linux 你想使用多少堆。
如果一个程序使用堆并且它使用的堆比可用的多,brk()
函数 returns 一些错误代码和 malloc()
函数只是 returns NULL
.
但是,这种情况的发生通常是因为没有更多的 RAM 可用,而不是因为堆与其他内存区域重叠。
... while the stack of the newly launched linux has grown downward to ...
Soon, there will be a clash/overlap of the memory addresses. What happens when the clash eventually occurs?
的确,栈的大小是有限的。
如果使用过多的堆栈,就会出现“堆栈溢出”。
此程序将故意使用过多堆栈 - 看看会发生什么:
.globl _start
_start:
sub [=10=]x100000, %rsp
push %rax
push %rax
jmp _start
如果操作系统带有 MMU(例如 Linux),您的程序将崩溃并显示错误消息:
~$ ./example_program
Segmentation fault (core dumped)
~$
EDIT/ADDENDUM
Is stack for all running programs located at the "end"?
在旧的 Linux 版本中,堆栈位于程序可访问的 虚拟 内存的末尾附近(但不完全位于):程序可以访问在那些 Linux 版本中,地址范围从 0
到 0xBFFFFFFF
。初始堆栈指针位于 0xBFFFFE00
附近。 (命令行参数和环境变量在堆栈之后。)
And is this the end of actual physical memory? Will not the stack of different running programs then get mixed up? I was under the impression that all of the stack and memory of a program remains contiguous in actual physical memory, ...
在使用 MMU 的计算机上,程序永远不会看到物理内存:
加载程序时,OS 将搜索 RAM 的一些空闲区域 - 也许它会在物理地址 0xABC000
找到一些。然后它以虚拟地址 0xBFFFF000-0xBFFFFFFF
转换为物理地址 0xABC000-0xABCFFF
.
的方式配置 MMU
这意味着:每当程序访问地址0xBFFFFE20
(例如使用push
操作)时,实际访问的是RAM中的物理地址0xABCE20
程序根本不可能访问某个物理地址。
如果您有另一个程序 运行ning,MMU 的配置方式是当另一个程序 [=170] 时地址 0xBFFFF000-0xBFFFFFFF
被转换为地址 0x345000-0x345FFF
=]宁.
所以如果两个程序中的一个要执行push
操作并且堆栈指针为0xBFFFFE20
,那么将访问RAM中的地址0xABCE20
;如果另一个程序执行push
操作(具有相同的堆栈指针值),将访问地址0x345E20
。
因此,堆叠不会混淆。
OS不使用 MMU 但支持 multi-tasking(示例是 Amiga 500 或早期的 Apple Macintoshes)当然不会以这种方式工作。这样的 OSs 使用特殊的文件格式(而不是 ELF),这些格式针对 运行ning 没有 MMU 的多个程序进行了优化。为这样的 OS 编译程序比为 Linux 或 Windows 编译程序要复杂得多。甚至对软件开发人员也有限制(例如:函数和数组不能太长)。
Also, does each program have its own stack pointer, base pointer, registers, etc.? Or does the OS just have one set of these registers to be shared by all programs?
(假设single-coreCPU),CPU有一组寄存器;并且只有一个程序可以同时运行。
当您启动多个程序时,OS会在程序之间切换。这意味着程序 A 运行s(例如)1/50 秒,然后程序 B 运行s 1/50 秒,然后程序 A 运行s 1/50 秒和很快。在您看来,好像程序 运行 同时出现。
当OS从程序A切换到程序B时,它必须先保存(程序A的)寄存器的值。然后它必须更改 MMU 配置。最后它必须恢复程序 B 的寄存器值。
我有一个 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
程序引用了.data
段中的字符串Hello , world!\n
,通过命令获得:
objdump -sj .data linux
Contents of section .data:
601030 00000000 00000000 00000000 00000000 ................
601040 48656c6c 6f202c20 776f726c 64210a00 Hello , world!..
所有这些告诉我,可执行文件已经创建,以便加载到从大约 0x0000000000400540
开始的实际内存地址(.init
的地址)并且程序访问实际内存中的数据地址至少扩展到 601040
(.data
的地址)
我基于 "Linkers & Loaders" by John R Levine 的第 7 章,他在其中指出:
A linker combines a set of input files into a single output file that is ready to be loaded at a specific address.
我的问题是关于下一行的。
If, when the program is loaded, storage at that address isn't available, the loader has to relocate the loaded program to reflect the actual load address.
(1) 假设我有另一个可执行文件当前在我的机器上 运行 已经在使用 400540
和 601040
之间的内存 space,它是如何决定的从哪里开始我的新可执行文件 linux
?
(2) 与此相关,在第4章中,说:
..ELF objects...are loaded in about the middle of the address space so the stack can grown down below the text segment and the heap can grow up from the end of the data, keeping the total address space in use relatively compact.
假设之前的 运行 应用程序开始于 200000
,现在 linux
开始于 400540
。内存地址没有冲突或重叠。但是随着程序的继续,假设前一个应用程序的堆上升到 300000
,而新启动的 linux
的堆已经下降到 310000
。很快,就会有一个clash/overlap的内存地址。当冲突最终发生时会发生什么?
是的,此可执行文件上的 objdump 显示了其段将被映射到的地址。 (链接将部分收集到段中:What's the difference of section and segment in ELF file format).data
和 .text
链接到具有不同权限的不同部分(读+写与读+执行)。
If, when the program is loaded, storage at that address isn't available
这只会在加载动态库时发生,而不是可执行文件本身。虚拟内存意味着每个进程都有自己的私有虚拟地址space,即使它们是从同一个可执行文件启动的。 (这也是为什么 ld
总是可以为 text
和 data
段选择相同的默认基地址,而不是试图将系统上的每个可执行文件和库放入一个不同的位置单个地址 space.)
当 OS 的 ELF 程序加载器 loaded/mapped 时,可执行文件是第一个声明该地址 space 部分的东西。这就是为什么传统的 (non-PIE) ELF 可执行文件可以是 non-relocatable,不像像 /lib/libc.so.6
如果您 single-step 一个带有调试器的程序,或者包含一个睡眠程序,您将有时间查看 less /proc/<PID>/maps
。或者 cat /proc/self/maps
让猫给你看它自己的地图。 (还有 /proc/self/smaps
有关每个映射的更多详细信息,例如有多少是脏的,使用大页面等)
(较新的 GNU/Linux 发行版默认将 GCC 配置为生成 PIE 可执行文件:32-bit absolute addresses no longer allowed in x86-64 Linux?。在这种情况下,objdump 只会看到相对于 0
或 1000
之类的。而且 compiler-generated asm 会使用 PC-relative 寻址,而不是绝对寻址。)
If, when the program is loaded, storage at that address isn't available, the loader has to relocate the loaded program to reflect the actual load address.
并非所有文件格式都支持:
32 位 GCC Windows 将在动态库 (.dll
) 的情况下添加加载程序所需的信息。但是,这些信息并没有添加到可执行文件中(.exe
),所以这样的可执行文件必须加载到固定地址。
Linux下有点复杂;但是,也不可能将许多(通常是较旧的 32 位)可执行文件加载到不同的地址,而动态库 (.so
) 可以加载到不同的地址。
Suppose I have another executable that is currently running on my machine already using the memory space between
400540
and601040
...
现代计算机(所有 x86 32 位计算机)都有一个分页 MMU,大多数现代操作系统都使用它。这是一些电路(通常在 CPU 中),它将软件看到的地址转换为 RAM 看到的地址。在您的示例中,400540
可以转换为 1234000
,因此访问地址 400540
实际上将访问 RAM 中的地址 1234000
。
重点是:现代 OS 对不同的任务使用不同的 MMU 配置。因此,如果您再次启动程序,将使用不同的 MMU 配置,将软件看到的地址 400540
转换为 RAM 中的地址地址 2345000
。使用地址 400540
的两个程序可以同时 运行 因为一个程序实际上访问地址 1234000
而另一个程序访问地址 2345000
地址 400540
.
这意味着加载可执行文件时某些地址(例如400540
)将永远不会“已在使用”。
加载动态库(.so
/.dll
)时该地址可能已被使用,因为这些库与可执行文件共享内存。
... how is it decided where to start my new executable linux?
在Linux下,如果可执行文件以无法移动到另一个地址的方式链接,则可执行文件将被加载到固定地址。 (如前所述:这对于较旧的 32 位文件来说很典型。)在您的示例中,“Hello world”字符串将位于地址 0x601040
if 您的编译器和链接器以这种方式创建了可执行文件。
但是,大多数 64 位可执行文件可以加载到不同的地址。 Linux 会将它们加载到一些 随机 地址,因为安全原因使病毒或其他恶意软件更难攻击程序。
... so the stack can grown down below the text segment ...
我从未在任何操作系统中见过这种内存布局:
在 Linux 和 Solaris 下,堆栈都位于地址 space 的末尾(在 0xBFFFFF00
附近),而文本段的加载非常接近内存的开始(可能是地址 0x401000
)。
... and the heap can grow up from the end of the data, ...
假设前一个应用程序的堆爬升...
自 1990 年代后期以来的许多实现不再使用堆。相反,他们使用 mmap()
来保留新内存。
根据 brk()
的手册页,堆在 2001 年被声明为“遗留功能”,因此新程序不应再使用它。
(但是,根据 Peter Cordes 的说法,malloc()
在某些情况下似乎仍然使用堆。)
不像MS-DOS这样的“简单”操作系统,Linux不允许您“简单地”使用堆,但您必须调用函数brk()
来告诉Linux 你想使用多少堆。
如果一个程序使用堆并且它使用的堆比可用的多,brk()
函数 returns 一些错误代码和 malloc()
函数只是 returns NULL
.
但是,这种情况的发生通常是因为没有更多的 RAM 可用,而不是因为堆与其他内存区域重叠。
... while the stack of the newly launched linux has grown downward to ...
Soon, there will be a clash/overlap of the memory addresses. What happens when the clash eventually occurs?
的确,栈的大小是有限的。
如果使用过多的堆栈,就会出现“堆栈溢出”。
此程序将故意使用过多堆栈 - 看看会发生什么:
.globl _start
_start:
sub [=10=]x100000, %rsp
push %rax
push %rax
jmp _start
如果操作系统带有 MMU(例如 Linux),您的程序将崩溃并显示错误消息:
~$ ./example_program
Segmentation fault (core dumped)
~$
EDIT/ADDENDUM
Is stack for all running programs located at the "end"?
在旧的 Linux 版本中,堆栈位于程序可访问的 虚拟 内存的末尾附近(但不完全位于):程序可以访问在那些 Linux 版本中,地址范围从 0
到 0xBFFFFFFF
。初始堆栈指针位于 0xBFFFFE00
附近。 (命令行参数和环境变量在堆栈之后。)
And is this the end of actual physical memory? Will not the stack of different running programs then get mixed up? I was under the impression that all of the stack and memory of a program remains contiguous in actual physical memory, ...
在使用 MMU 的计算机上,程序永远不会看到物理内存:
加载程序时,OS 将搜索 RAM 的一些空闲区域 - 也许它会在物理地址 0xABC000
找到一些。然后它以虚拟地址 0xBFFFF000-0xBFFFFFFF
转换为物理地址 0xABC000-0xABCFFF
.
这意味着:每当程序访问地址0xBFFFFE20
(例如使用push
操作)时,实际访问的是RAM中的物理地址0xABCE20
程序根本不可能访问某个物理地址。
如果您有另一个程序 运行ning,MMU 的配置方式是当另一个程序 [=170] 时地址 0xBFFFF000-0xBFFFFFFF
被转换为地址 0x345000-0x345FFF
=]宁.
所以如果两个程序中的一个要执行push
操作并且堆栈指针为0xBFFFFE20
,那么将访问RAM中的地址0xABCE20
;如果另一个程序执行push
操作(具有相同的堆栈指针值),将访问地址0x345E20
。
因此,堆叠不会混淆。
OS不使用 MMU 但支持 multi-tasking(示例是 Amiga 500 或早期的 Apple Macintoshes)当然不会以这种方式工作。这样的 OSs 使用特殊的文件格式(而不是 ELF),这些格式针对 运行ning 没有 MMU 的多个程序进行了优化。为这样的 OS 编译程序比为 Linux 或 Windows 编译程序要复杂得多。甚至对软件开发人员也有限制(例如:函数和数组不能太长)。
Also, does each program have its own stack pointer, base pointer, registers, etc.? Or does the OS just have one set of these registers to be shared by all programs?
(假设single-coreCPU),CPU有一组寄存器;并且只有一个程序可以同时运行。
当您启动多个程序时,OS会在程序之间切换。这意味着程序 A 运行s(例如)1/50 秒,然后程序 B 运行s 1/50 秒,然后程序 A 运行s 1/50 秒和很快。在您看来,好像程序 运行 同时出现。
当OS从程序A切换到程序B时,它必须先保存(程序A的)寄存器的值。然后它必须更改 MMU 配置。最后它必须恢复程序 B 的寄存器值。