程序编译
1、源程序如.c/.cpp
2、经过预处理器,得到被修改后的源程序.i:
预处理器可以删除注释、包含其他文件以及执行宏(宏macro是一段重复文字的简短描写)替代。
3、经过编译器,得到汇编文本.s
4、经过汇编器,得到可重定位目标程序,二进制文件.o
5、经过链接器,如printf.o加入,得到可执行目标程序
6、程序存放在磁盘中或加载到内存中执行
可执行目标文件格式
一个典型的ELF可执行文件中各类信息:
ELF头部描述了文件格式,包括程序的入口点等;
ELF可执行文件加载到内存;加载—execve函数调用加载器—将文件代码+数据从磁盘拷到内存;然后跳转到程序入口点。
程序加载
如何利用虚拟存储器将磁盘上代码+数据映射到内存中?
加载是一次全部加载进去还是一部分一部分加载进去?请看Linux文件系统。
程序有自己的存储器映像,而32位Linux上代码段总是从0x08048000开始;数据段在4KB对齐地址上;运行时堆在读/写段之后接下来的第一个4KB对齐地址处,并调用malloc而增长;而用户栈自顶向下生长,是用户进程空间中的一块区域,用于保存用户进程的子程序间相互调用的参数、返回值、返回点以及子程序(函数)的局部变量。
程序运行
- 内核在建立一个进程时首先要将其段寄存器都设置好。
具体即:CS:index=4, TI=0(使用GDT), RPL=3(用户特权);DS-ES-SS: index=5, TI=0(使用GDT), RPL=3(用户特权);切换到内核态,则CS: index=2,TI=0, RPL=0; DS-ES-SS: index=3, TI=0, RPL=0。
通过此寄存器得到GDT表(所有进程共用)的2,3,4,5项,其基地址均为0,上限4G,不同在于DPL表示特权等级,S表示类型,type表示属性。DPL与段的RPL进行核对,CS不能访问数据段进行核对。其实这核对意义不大,页式映射时也要比对,无非是MMU要求而走流程。 - 之后页面映射:
每个进程都有自己的PGD,对应指针在mm_struct中,调入程序执行时设置好CR3寄存器。(注意,设置CR3也需要程序,那么设置完后该程序上下页面不同?事实上不会,因为内核空间中有相同的页面映射,不依赖于CR3)
注意:TLB/后备缓冲器是一个内存管理单元,是用于改进虚拟地址到物理地址转换速度的缓存。其中每一行都保存着一个由单个PTE组成的块。所以,当cpu要访问一个虚拟地址/线性地址时,CPU会首先根据虚拟地址的高20位在TLB中查找。如果是表中没有相应的表项,称为TLB miss,需要通过访问慢速RAM中的页表计算出相应的物理地址。同时,物理地址被存放在一个TLB表项中,以后对同一线性地址的访问,直接从TLB表项中获取物理地址即可,称为TLB hit。然后根据PTE所得页框地址+后12位地址得到实际物理地址。 - 加载器跳转到程序的入口点,也就是符号_start的地址,此处的启动代码是在目标文件ctrl.o定义。在.text和.init中调用了初始化例程后,启动代码调用atexit例程——应用程序正常中止应该调用的程序。再调用main程序,再exit函数运行atexit注册的函数,并调用_exit将控制权返回给操作系统。
CPU中程序计数器根据代码段不断得到指令地址,或向下(+4)或跳转;CPU将指令由内存取到指令寄存器中执行,这时便涉及到虚拟地址/逻辑地址到物理地址的转换。
注意:程序计数器PC即指令指针寄存器IP,或者又作为段偏移地址寄存器,它们都是存储下一条执行指令的地址。x86上一般叫IP,ARM上叫PC/R15。在x86上不能直接给IP赋值,可以通过jmp来改变它的值;在ARM上可以通过LDR直接对PC赋值。
进程切换
一个进程在执行时,CPU的所有寄存器中的值、进程的状态以及堆栈中的内容被称为该进程的上下文。
一个进程的上下文可以分为三个部分:用户级上下文、寄存器上下文以及系统级上下文。
用户级上下文: 正文、数据、用户堆栈以及共享存储区;
寄存器上下文: 通用寄存器、程序寄存器(IP)、处理器状态寄存器(EFLAGS)、栈指针(ESP);
系统级上下文: 进程控制块task_struct、内存管理信息(mm_struct、vm_area_struct、pgd、pte)、内核栈
当内核需要切换到另一个进程时,它需要保存当前进程的所有状态,即保存当前进程的上下文,以便在再次执行该进程时,能够必得到切换时的状态执行下去。
在LINUX中,当前进程上下文均保存在进程的任务数据结构中。在发生中断时,内核就在被中断进程的上下文中,在内核态下执行中断服务例程。但同时会保留所有需要用到的资源,以便中继服务结束时能恢复被中断进程的执行。
参考文章:CSAPP