为什么 ELF 执行入口点虚拟地址的形式为 0x80xxxxx 而不是零 0x0?
执行时,程序将从虚拟地址0x80482c0开始运行。该地址并不指向我们的 main()
过程,而是指向由链接器创建的名为 _start
的过程。
到目前为止,我的谷歌研究只是让我做出了一些(模糊的)历史推测,如下所示:
有民间传说,0x08048000 曾经是 STACK_TOP(即堆栈从 0x08048000 附近向下向 0 增长),在 *NIX 到 i386 的端口上,这是由加利福尼亚州圣克鲁斯的一个组织发布的。当时 128MB RAM 非常昂贵,4GB RAM 更是不可想象。
有人能证实/否认这一点吗?
When executed, program will start running from virtual address 0x80482c0. This address doesn't point to our main()
procedure, but to a procedure named _start
which is created by the linker.
My Google research so far just led me to some (vague) historical speculations like this:
There is folklore that 0x08048000 once was STACK_TOP (that is, the stack grew downwards from near 0x08048000 towards 0) on a port of *NIX to i386 that was promulgated by a group from Santa Cruz, California. This was when 128MB of RAM was expensive, and 4GB of RAM was unthinkable.
Can anyone confirm/deny this?
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
正如 Mads 指出的,为了捕获大多数通过空指针的访问,类 Unix 系统倾向于使地址为零的页面“未映射”。因此,访问会立即触发 CPU 异常,即段错误。这比让应用程序失控要好得多。然而,异常向量表可以位于任何地址,至少在 x86 处理器上(有一个特殊的寄存器,加载有
lidt
操作码)。起始点地址是描述内存如何布局的一组约定的一部分。链接器在生成可执行二进制文件时必须知道这些约定,因此它们不太可能更改。基本上,对于 Linux,内存布局约定是从 90 年代初的 Linux 的第一个版本继承的。进程必须能够访问多个区域:
brk()
和sbrk()
系统调用而增加。mmap()
系统调用,包括共享库加载。如今,
malloc()
所在的堆由mmap()
调用支持,这些调用在内核认为合适的任何地址获取内存块。但在更早的时代,Linux 就像以前的类 Unix 系统一样,它的堆需要一个不间断的块中的一个大区域,它可以朝着增加地址的方向增长。因此,无论约定是什么,它都必须向低地址填充代码和堆栈,并将给定点之后的每个地址空间块都分配给堆。但还有堆栈,它通常很小,但在某些情况下可能会急剧增长。堆栈向下增长,当堆栈已满时,我们确实希望进程可预测地崩溃,而不是覆盖某些数据。因此,堆栈必须有一个宽阔的区域,在该区域的低端有一个未映射的页面。瞧!地址零处有一个未映射的页面,用于捕获空指针取消引用。因此,定义堆栈将获取前 128 MB 地址空间(第一页除外)。这意味着代码必须在类似于 0x080xxxxx 的地址处处理这 128 MB。
正如 Michael 指出的那样,“丢失”128 MB 地址空间没什么大不了的,因为相对于实际可用的地址空间而言,地址空间非常大。当时,Linux 内核将单个进程的地址空间限制为 1 GB,超过了硬件允许的最大 4 GB,这不被认为是一个大问题。
As Mads pointed out, in order to catch most accesses through null pointers, Unix-like systems tend to make the page at address zero "unmapped". Thus, accesses immediately trigger a CPU exception, in other words a segfault. This is quite better than letting the application go rogue. The exception vector table, however, can be at any address, at least on x86 processors (there is a special register for that, loaded with the
lidt
opcode).The starting point address is part of a set of conventions which describe how memory is laid out. The linker, when it produces an executable binary, must know these conventions, so they are not likely to change. Basically, for Linux, the memory layout conventions are inherited from the very first versions of Linux, in the early 90's. A process must have access to several areas:
brk()
andsbrk()
system calls.mmap()
system calls, including shared library loading.Nowadays, the heap, where
malloc()
goes, is backed bymmap()
calls which obtain chunks of memory at whatever address the kernel sees fit. But in older times, Linux was like previous Unix-like systems, and its heap required a big area in one uninterrupted chunk, which could grow towards increasing addresses. So whatever was the convention, it had to stuff code and stack towards low addresses, and give every chunk of the address space after a given point to the heap.But there is also the stack, which is usually quite small but could grow quite dramatically in some occasions. The stack grows down, and when the stack is full, we really want the process to predictably crash rather than overwriting some data. So there had to be a wide area for the stack, with, at the low end of that area, an unmapped page. And lo! There is an unmapped page at address zero, to catch null pointer dereferences. Hence it was defined that the stack would get the first 128 MB of address space, except for the first page. This means that the code had to go after those 128 MB, at an address similar to 0x080xxxxx.
As Michael points out, "losing" 128 MB of address space was no big deal because the address space was very large with regards to what could be actually used. At that time, the Linux kernel was limiting the address space for a single process to 1 GB, over a maximum of 4 GB allowed by the hardware, and that was not considered to be a big issue.
为什么不从地址0x0开始呢?至少有两个原因:
至于入口点
_start
与main
:如果您链接到 C 运行时(C 标准库),该库会包装名为
main
的函数,因此它可以在调用main
之前初始化环境。在 Linux 上,这些是应用程序的 argc 和 argv 参数、env 变量,以及可能的一些同步原语和锁。它还确保从 main 返回传递状态代码,并调用 _exit 函数来终止进程。Why not start at address 0x0? There's at least two reasons for this:
As for the entrypoint
_start
vsmain
:If you link against the C runtime (the C standard libraries), the library wraps the function named
main
, so it can initialize the environment beforemain
is called. On Linux, these are the argc and argv parameters to the application, the env variables, and probably some synchronization primitives and locks. It also makes sure that returning from main passes on the status code, and calls the_exit
function, which terminates the process.