运行程序时会发生什么?

45
我希望在这里收集有关在Windows,Linux和OSX上运行可执行文件时发生的情况。特别是,我想确切了解操作顺序:我的猜测是内核加载可执行文件格式(PE,ELF或Mach-O)(但我不知道ELF(可执行和可链接格式)的各个部分及其含义),然后动态链接器解析引用,然后运行可执行文件的__init部分,然后是主要部分,然后是__fini,最后完成程序,但我确定这非常粗略,并且可能是错误的。

编辑:问题现在是CW。 我正在为Linux填充。 如果有人想做相同的Win和OSX,那就太好了。


1
这个问题的范围是不是太广了? - mezoid
我不认为这个问题太宽泛,但可能应该设为社区维基。 - Jeff Leonard
如果我没有得到足够的反馈,我想对此进行悬赏。如果它是 CW 的话,我将无法这样做。 - Stefano Borini
我同意这可能比较宽泛。事实上,我想为系统之间的比较奠定基础。我接受建议。 - Stefano Borini
仅限Linux:https://dev59.com/questions/Lmsy5IYBdhLWcg3wvghl - Ciro Santilli OurBigBook.com
5个回答

37

当然,这只是一个非常高级和抽象的层面!

Executable - No Shared Libary: 

Client request to run application
  ->Shell informs kernel to run binary
  ->Kernel allocates memory from the pool to fit the binary image into
  ->Kernel loads binary into memory
  ->Kernel jumps to specific memory address
  ->Kernel starts processing the machine code located at this location
  ->If machine code has stop
  ->Kernel releases memory back to pool

Executable - Shared Library

Client request to run application
  ->Shell informs kernel to run binary
  ->Kernel allocates memory from the pool to fit the binary image into
  ->Kernel loads binary into memory
  ->Kernel jumps to specific memory address
  ->Kernel starts processing the machine code located at this location
  ->Kernel pushes current location into an execution stack
  ->Kernel jumps out of current memory to a shared memory location
  ->Kernel executes code from this shared memory location
  ->Kernel pops back the last memory location and jumps to that address
  ->If machine code has stop
  ->Kernel releases memory back to pool

JavaScript/.NET/Perl/Python/PHP/Ruby (Interpretted Languages)

Client request to run application
  ->Shell informs kernel to run binary
  ->Kernel has a hook that recognises binary images needs a JIT
  ->Kernel calls JIT
  ->JIT loads the code and jumps to a specific address
  ->JIT reads the code and compiles the instruction into the 
    machine code that the interpretter is running on
  ->Interpretture passes machine code to the kernel
  ->kernel executes the required instruction
  ->JIT then increments the program counter
  ->If code has a stop
  ->Jit releases application from its memory pool

正如routeNpingme所说,寄存器是在CPU内部设置的,其中神奇的事情发生了!

更新: 是的,今天我无法正确拼写!


内核将二进制文件加载到内存中 ->内核跳转到特定的内存地址 "内核从这个共享内存位置执行代码" 我对此表示怀疑。内核非常谨慎地选择要执行的代码;通常情况下,它不会执行用户空间代码。你说的可以很容易被攻击者利用。 Stefano 的回答更有意义。 - Infinite

34

好的,我来回答自己的问题。这将逐步完成,仅针对Linux(也许是Mach-O)。随意添加更多内容到您的个人答案中,以便获得赞(并且您可以获得徽章,因为现在它是CW)。

我会从中间开始,并随着发现构建其余部分。此文档是使用x86_64、gcc(GCC)4.1.2制作的。

打开文件、初始化

在本节中,我们描述了程序被调用时从内核的角度发生了什么,直到程序准备好执行为止。

  1. 打开ELF文件。
  2. 内核寻找.text段并将其加载到内存中。标记为只读。
  3. 内核加载.data段。
  4. 内核加载.bss段,并将所有内容初始化为零。
  5. 内核将控制权转移到动态链接器(其名称在ELF文件中,在.interp部分中)。动态链接器解析所有共享库调用。
  6. 控制权转移到应用程序。

程序的执行

  1. 函数_start被调用,因为ELF头将其指定为可执行文件的入口点
  2. _start通过PLT调用glibc中的__libc_start_main,向其传递以下信息:

    1. 实际main函数的地址
    2. argc地址
    3. argv地址
    4. _init例程的地址
    5. _fini例程的地址
    6. atexit()注册的函数指针
    7. 可用的最高堆栈地址
  3. _init被调用

    1. 调用call_gmon_start来初始化gmon分析。与执行无关。
    2. 调用frame_dummy,它包装__register_frame_info(eh_frame段地址、bss段地址)(FIXME:这个函数做什么?显然是从BSS段初始化全局变量)
  4. 调用 __do_global_ctors_aux 函数,其作用是调用 .ctors 段中列出的所有全局构造函数。
  5. main 函数被调用。
  6. main 函数结束。
  7. _fini 被调用,它又会调用 __do_global_dtors_aux 函数来运行在 .dtors 段中指定的所有析构函数。
  8. 程序退出。

1
我不知道你想要多详细的内容,但是我很难理解这个问题,因为我不知道ELF是什么(或者说Linux在底层上与我想象中的非常不同)。 - John Fouhy
我会在有时间继续阅读我找到的文档后继续这一部分。 ELF 是 Linux 下可执行文件的二进制格式。类似于 Windows 中的 PE 和 OSX 中的 Mach-O。 - Stefano Borini

5
在Windows系统上,首先将图像加载到内存中。内核会分析它需要的库(也称为“DLL”),并将它们加载起来。然后,它会编辑程序图像,插入每个库函数所需的内存地址。这些地址在.EXE二进制文件中已经有了一个空间,但是它们只是填充了零。
然后,按照依赖关系的顺序,从最重要的DLL到最后一个,依次执行每个DLL的DllMain()过程。
一旦所有库都被加载并准备好了,最后启动图像,接下来发生的任何事情都取决于使用的语言、编译器和程序本身。

2

一旦图像被加载到内存中,就会自动触发魔法操作。


那是如果你将其设置为“魔法”。 “更多魔法”会破坏宇宙。 - jkeys

0

嗯,根据您的确切定义,您必须考虑像 .Net 和 Java 这样的语言的 JIT 编译器。当您运行一个不是技术上的“可执行文件”的 .Net “exe” 时,JIT 编译器会介入并编译它。


3
.Net运行时是一个可执行文件...它运行整个虚拟环境并优化字节码的事实不重要。 - Tal Pressman

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