Linux Executable file: Structure & Running 2
可执行文件的运行
要运行可执行目标文件 prog,我们可以在 Linux shell 的命令行中输入它的名字:linux> ./prog
因为 prog 不是一个内置的 shell 命令,所以 shell 会认为 prog 是一个可执行目标文件。
进程的启动
- Linux进程的启动是通过父进程复制一个子进程,子进程通过execve系统调用启动加载器。
- 加载器(loader)删除子进程已有的虚拟存储段,
- 通过将虚拟地址空间中的页映射到可执行文件的页大小组块,
- 并创建一组新的代码、数据、堆、栈段,
- 同时新的堆和栈被初始化为零。
- 新的代码和数据段被初始化为可执行文件的内容,
- 最后将CUP指令寄存器设置成可执行文件入口,启动运行。
执行完上述操作后,其实可执行文件的真正指令和数据都没有别装入内存中。操作系统只是通过可执行文件头部的信息建立起可执行文件和进程虚拟内存之间的映射关系而已。
除了一些头部信息,在加载过程中没有任何从磁盘到内存的数据复制。直到 CPU 引用一个被映射的虚拟页时才会进行复制,此时,操作系统利用它的页面调度机制自动将页面从磁盘传送到内存。
比如,现在程序的入口地址为 0x08048000
,刚好是代码段的起始地址。当CPU打算执行这个地址的指令时,发现页面 0x8048000
~ 0x08049000
(一个页面一般是4K)是个空页面,于是它就认为是个页错误。此时操作系统根据虚拟地址空间与可执行文件间的映射关系找到页面在可执行文件中的偏移,然后在物理内存中分配一个物理页面,并在虚拟地址页面与物理页面间建立映射,最后把EXE文件中页面拷贝到内存的物理页面,进程重新开始执行。该过程如下图所示:
接下来,加载器跳转到程序的入口点,也就是 _start
函数的地址。这个函数是在系统目标文件 ctrl.o 中定义的,对所有的 C 程序都是一样的。_start 函数调用系统启动函数 __libc_start_main
,该函数定义在 libc.so
中。它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回给内核。
fork 和 execve 函数的差异
- fork 函数在新的子进程中运行相同的程序,新的子进程是父进程的一个复制品。
- execve 函数在当前进程的上下文中加载并运行一个新的程序。
- 它会覆盖当前进程的地址空间,但并没有创建一个新进程。
- 新的程序仍然有相同的 PID,并且继承了调用 execve 函数时已打开的所有文件描述符。
程序运行途中修改exe程序
- 由于操作系统使用页表和虚拟内存机制来实现按需加载。按需加载意味着只有在程序执行到需要访问某个代码段时,才会将该代码段从可执行文件加载到内存中。
- 那么如果我在程序运行的途中重新编译程序,修改了代码段,那么程序会怎么样呢?
- Chatgpt:运行中的程序尝试执行新的代码时,会发生未定义的行为,因为操作系统不会自动将新的代码加载到正在运行的进程的内存中。
- 一个页表4KB,一个程序的代码段可能有如下100KB甚至几十MB,不可能全部加载。
1 | # shaojiemike @ snode6 in ~/github/sniper_PIMProf/PIMProf/gapbs on git:dev o [15:15:29] |
程序运行途中修改python代码
- 虽然修改python代码类似修改C代码,按理来说不会影响程序进行。但是python是逐行解释执行的,很难让人不思考会不会影响正在运行中的程序。
- 答案是不会,原因有二:
- python代码在运行时,会被编译成字节码,然后再执行字节码。修改python代码后,其对应的字节码会在下一次运行程序时,Python解释器对比文件时间戳时更新。
- Python解释器在运行时,会将所需的文件提前加载到内存里
GDB调试修改
Linux Executable file: Structure & Running 2
http://icarus.shaojiemike.top/2023/08/07/Work/Operating system/Executablefilerunningprocess/