摘 要
每一位程序员都对hello熟悉不已,hello是我们走向又爱又恨的计算机专业的开始。hello并不是像那几行代码那样简单,究其根本,它要经过预处理,编译,汇编,链接等一系列步骤才能成为一个可执行文件。真正运行这个程序又要应用程序,操作系统,硬件系统的紧密联系。本文将重走hello一生,感受计算机的魅力。
关键词:hello;预处理;编译;汇编;链接;进程;
目 录
第1章 概述………………………………………………………………………………………………. – 4 –
1.1 Hello简介………………………………………………………………………………………… – 4 –
1.2 环境与工具……………………………………………………………………………………….. – 4 –
1.3 中间结果…………………………………………………………………………………………… – 4 –
1.4 本章小结…………………………………………………………………………………………… – 4 –
第2章 预处理…………………………………………………………………………………………… – 5 –
2.1 预处理的概念与作用…………………………………………………………………………. – 5 –
2.2在Ubuntu下预处理的命令……………………………………………………………….. – 5 –
2.3 Hello的预处理结果解析…………………………………………………………………… – 5 –
2.4 本章小结…………………………………………………………………………………………… – 5 –
第3章 编译………………………………………………………………………………………………. – 6 –
3.1 编译的概念与作用…………………………………………………………………………….. – 6 –
3.2 在Ubuntu下编译的命令………………………………………………………………….. – 6 –
3.3 Hello的编译结果解析………………………………………………………………………. – 6 –
3.4 本章小结…………………………………………………………………………………………… – 6 –
第4章 汇编………………………………………………………………………………………………. – 7 –
4.1 汇编的概念与作用…………………………………………………………………………….. – 7 –
4.2 在Ubuntu下汇编的命令………………………………………………………………….. – 7 –
4.3 可重定位目标elf格式……………………………………………………………………… – 7 –
4.4 Hello.o的结果解析………………………………………………………………………….. – 7 –
4.5 本章小结…………………………………………………………………………………………… – 7 –
第5章 链接………………………………………………………………………………………………. – 8 –
5.1 链接的概念与作用…………………………………………………………………………….. – 8 –
5.2 在Ubuntu下链接的命令………………………………………………………………….. – 8 –
5.3 可执行目标文件hello的格式………………………………………………………….. – 8 –
5.4 hello的虚拟地址空间………………………………………………………………………. – 8 –
5.5 链接的重定位过程分析……………………………………………………………………… – 8 –
5.6 hello的执行流程……………………………………………………………………………… – 8 –
5.7 Hello的动态链接分析………………………………………………………………………. – 8 –
5.8 本章小结…………………………………………………………………………………………… – 9 –
第6章 hello进程管理……………………………………………………………………….. – 10 –
6.1 进程的概念与作用…………………………………………………………………………… – 10 –
6.2 简述壳Shell-bash的作用与处理流程…………………………………………….. – 10 –
6.3 Hello的fork进程创建过程…………………………………………………………… – 10 –
6.4 Hello的execve过程……………………………………………………………………… – 10 –
6.5 Hello的进程执行……………………………………………………………………………. – 10 –
6.6 hello的异常与信号处理…………………………………………………………………. – 10 –
6.7本章小结…………………………………………………………………………………………. – 10 –
第7章 hello的存储管理…………………………………………………………………….. – 11 –
7.1 hello的存储器地址空间…………………………………………………………………. – 11 –
7.2 Intel逻辑地址到线性地址的变换-段式管理…………………………………….. – 11 –
7.3 Hello的线性地址到物理地址的变换-页式管理………………………………… – 11 –
7.4 TLB与四级页表支持下的VA到PA的变换……………………………………… – 11 –
7.5 三级Cache支持下的物理内存访问…………………………………………………. – 11 –
7.6 hello进程fork时的内存映射………………………………………………………… – 11 –
7.7 hello进程execve时的内存映射…………………………………………………….. – 11 –
7.8 缺页故障与缺页中断处理………………………………………………………………… – 11 –
7.9动态存储分配管理…………………………………………………………………………… – 11 –
7.10本章小结……………………………………………………………………………………….. – 12 –
第8章 hello的IO管理……………………………………………………………………… – 13 –
8.1 Linux的IO设备管理方法……………………………………………………………….. – 13 –
8.2 简述Unix IO接口及其函数…………………………………………………………….. – 13 –
8.3 printf的实现分析……………………………………………………………………………. – 13 –
8.4 getchar的实现分析………………………………………………………………………… – 13 –
8.5本章小结…………………………………………………………………………………………. – 13 –
结论…………………………………………………………………………………………………………. – 14 –
附件…………………………………………………………………………………………………………. – 15 –
参考文献………………………………………………………………………………………………….. – 16 –
第1章 概述
1.1 Hello简介
1.在编译器或其它工具上编写hello.c的代码并保存,这就是源文件。
2.预处理器根据hello.c中的“#”开头的命令进行处理,得到hello.i文件。
3.编译器将hello.i翻译成汇编语言程序文件hello.s
4.汇编器将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标文件,并将结果保存至文件hello.i中。
5.链接器合并标准库的一些文件,得到可执行目标文件hello。
6.shell是一个命令行解释器。输入./hello后,shell通过fork创建子进程,在该进程中通过execve加载并执行可执行目标文件hello。
7.将hello的代码和数据从磁盘加载到主存后,处理器开始执行hello中的机器语言。
8. CPU执行的机器语言指令将hello程序中的字符串中的字节从主存复制到寄存器文件,再从寄存器文件中复制到显示设备,最终显示在屏幕上。
9.hello程序运行结束后,其进程终止,操作系统恢复进程的上下文,并传回控制权。
10.shell回收终止的hello进程,操作系统删除数据和代码。
1.2 环境与工具
硬件环境:X86-64;inter core i5;
软件环境VMware;Ubuntu18.04;
开发工具:vim;gedit;codeblocks;edb;
1.3 中间结果
文件名 | 文件作用 |
hello.c | 源程序文件 |
hello.i | 预处理后文件 |
hello.s | 编译后文件 |
hello.o | 汇编后文件(可重定位目标文件) |
hello | 链接后文件(可执行目标文件) |
1.4 本章小结
本章简述了hello从0到1的全过程,还说明了软件硬件环境及开发工具,并列出了本文生成的中间文件。
第2章 预处理
2.1 预处理的概念与作用
预处理的概念:预处理器根据以字符#开头的命令,修改原始的C程序。
预处理的作用:宏定义,文件包含关系,条件编译
2.2在Ubuntu下预处理的命令
使用gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
可以看到,预处理后文件有3105行,原因是:预处理器将源程序中的头文件进行了修改,修改为相应函数。
2.4 本章小结
本章阐释了预处理的概念及作用,并通过虚拟机在Ubuntu下进行了文件预处理,生成了hello.i文件。
第3章 编译
3.1 编译的概念与作用
编译的概念:1、利用编译程序从源语言编写的源程序产生目标程序的过程。 2、用编译程序产生目标程序的动作。
编译的作用:编译过程分为五个阶段:词法分析;语法分析;语义检查和中间代码生成;代码优化;目标代码生成。主要是进行词法分析和语法分析,又称为源程序分析,分析过程中发现有语法错误,给出提示信息。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
在Ubuntu下通过命令gcc -S hello.i -o hello.i生成hello.i文件
3.3 Hello的编译结果解析
3.3.1数据:
.LC0中的是源代码printf(“用法: Hello 学号 姓名 秒数!\n”);中的字符串;
.LC1中的是源代码printf(“Hello %s %s\n”,argv[1],argv[2]);中的格式控制字符。
例如:
常量4(立即数)存储在代码段,而argc存储在寄存器%edi中;
局部变量i直到真正使用它的时候才会真正出现
3.3.2赋值:
mov类指令实现赋值,例如:
3.3.3类型转换:
b,w,l,q为四种不同的指令后缀,不同的后缀往往对应不同大小的数据的操作。不同的寄存器名称也体现不同的大小的数据。%rax8字节,%eax4字节。当类型转换时,可用相应的指令。Movs和movz类指令可实现类型转换。
3.3.4sizeof:
b,w,l,q为四种不同的指令后缀,不同的后缀往往对应不同大小的数据的操作。不同的寄存器名称也体现不同的大小的数据。%rax8字节,%eax4字节。
3.3.5算术运算:
算术运算可用lea指令实现,或者计算类指令,如:add,sub, xor ,or and,neg等
下为一个加运算
3.3.6逻辑/位操作:
逻辑操作在x86-64中也可有指令实现:SAL,SHL,SAR,SHR
3.3.7关系操作:
COU中有条件吗寄存器,描述了最近的运算的属性,可以通过特定的条件码及其组合表示相应关系,如:ZF代表相等
3.3.8数组/指针/结构操作:
一般数组采用比例变址寻址。指针和结构也采用类似的基址+偏移量的寻址方式。如:
3.3.9控制转移:
跳转指令或条件转移指令通过检测条件码寄存器来实现控制转移。
例如:
3.3.10函数操作:
call指令实现函数的调用,ret实现从被调用函数中返回。例如:
传参不高于六个,可通过寄存器进行传参,超过六个的部分要通过栈进行传递,%rax中保存者返回值。
3.4 本章小结
本章阐述了编译的概念和作用,并在Ubuntu中进行了编译。同时分析反汇编代码,从数据、赋值、类型转换、sizeof、算术和逻辑操作、关系操作、数组操作、控制转移、函数操作等方面,说明了编译器对C语言各个数据类型以及各类操作的处理。
第4章 汇编
4.1 汇编的概念与作用
汇编的概念:将汇编语言翻译为机器语言。
汇编的作用:汇编器将hello.s翻译成机器指令,并打包成可重定位目标文件hello.o,便于后续机器的直接运行
注意:这儿的汇编是指从 .s 到 .o 即编译后的文件到生成机器语言二进制程序的过程。
4.2 在Ubuntu下汇编的命令
通过指令gcc -c hello.s -o hello.o实现汇编
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
使用指令readlf -a hello.o查看hello.o
hello.o的ELF格式
ELF头的开始描述了生成该文件的系统的字的大小和字节顺序。剩下的部分包含帮助链接器语法分析和解释目标文件的信息。其中包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
节头表如下:节头部表描述了每个节的名称、类型、地址、偏移量、以及flag、链接、信息和Align等信息
重定位节信息如下:它告诉链接器在将目标文件合并成可执行文件时如何修改目标位置
第一个.rodata -4是对.L0所表示的字符串进行重定位,然后是对puts函数、exit函数,.L1所表示的第二个字符串、printf函数、atoi函数、sleep函数、getchar函数进行重定位重定位分为PC相对寻址和PC绝对寻址,可根据上图信息利用公式计算出重定位后地址。
符号表:存放的是程序调用的函数以及所定义的变量的信息
4.4 Hello.o的结果解析
objdump -d -r hello.o 分析hello.o的反汇编,并请与第3章的 hello.s进行对照分析。
将其和hello.s对照可得:
- 操作数:hello.s为十进制
hello.o的反汇编为16进制
- 分支调用:
在hello.s中会使用.L2之类的标记,这些标记便于程序员及汇编器理解,汇编之后丢失
在hello.o的反汇编中使用地址+偏移的表示方法
- 函数调用:
在hello.s中会直接使用函数名调用
在hello.o的反汇编中需要链接之后才能知确切地址,实际指令部分地址为0
4.5 本章小结
本章首先阐述了汇编的概念和作用,在Ubuntu下实现了hello.s到hello.o的汇编。接着分析了ELF文件格式各部分及跟不过分包含的信息。最后对比了hello.s和hello.o反汇编代码的不同。
第5章 链接
5.1 链接的概念与作用
链接的概念:链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
链接的作用:链接器在软件开发中扮演者一个关键的角色,因为它们使得分离编译成为可能。我们不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
注意:这儿的链接是指从 hello.o 到hello生成过程。
5.2 在Ubuntu下链接的命令
在Ubuntu下使用ld -o hello -dynamic-linker /lib64/ld-linux-x86-64.so.2 /usr/lib/x86_64-linux-gnu/crt1.o /usr/lib/x86_64-linux-gnu/crti.o hello.o /usr/lib/x86_64-linux-gnu/libc.so /usr/lib/x86_64-linux-gnu/crtn.o 进行链接
5.3 可执行目标文件hello的格式
分析hello的ELF格式,用readelf等列出其各段的基本信息,包括各段的起始地址,大小等信息。
ELF可执行目标文件格式:可执行目标文件的格式类似于可重定位目标文件的格式,ELF头描述文件的总体格式,它还包括程序的入口点,也就是当程序运行时要执行的第一条指令的地址。
节头部表,符号表与可执行目标文件格式类似,只不过链接后包含更多内容
ELF可执行文件被设计得很容易加载到内存,可执行文件的连续的片被映射到连续的内存段。程序头部表描述了这种映射关系:
5.4 hello的虚拟地址空间
使用edb加载hello,查看本进程的虚拟地址空间各段信息,并与5.3对照分析说明。
1.使用edb
2. 从0x400000-0x401000之间的节对应节表头声明
通过Data Dump窗口查看虚拟地址段0x600000到0x602000的部分,在0到fff的空间中,与0x400000到0x401000段的存放的程序相同;而在fff之后存放的是.dynamic到.shstrtab节。
5.5 链接的重定位过程分析
1.使用objdump -d -r hello获得hello的反汇编代码。
2.易见hello的反汇编内容多了很多,主要是新增的库函数和.init.节。新增了许多外部链接的共享库函数,如printf@plt函数,getchar@plt函数
跳转和函数调用的地址在hello的反汇编代码中使用的是虚拟内存地址:
5.6 hello的执行流程
使用edb执行hello,说明从加载hello到_start,到call main,以及程序终止的所有过程。请列出其调用与跳转的各个子程序名或程序地址。
5.7 Hello的动态链接分析
dl_init前
dl_init后
上为动态链接延迟绑定的初始化部分。延迟绑定将过程地址的绑定推迟到第一次调用该过程时。延迟绑定通过GOT和过程链接表PLT的协同工作实现函数的动态链接,其中GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。绑定后,每次都直接跳转到目标函数的地址。
5.8 本章小结
本章阐述了链接的概念及作用,又在Ubuntu下实际操作,然后分析hello的格式,重定位等内容的分析。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:进程是对处理器,主存,I/O设备的抽象表示。进程的经典定义就是一个执行中程序的实例。
进程的作用:进程提供给应用程序两个关键的抽象:一个独立的逻辑控制流,它提供一个假象,好像我们的程序独占地使用处理器;一个私有地地址空间,它提供一个假象,好像我们的程序独占地使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell:shell是一个交互型的应用及程序。
功能作用:实际上Shell是一个命令解释器,它解释由用户输入的命令并且把它们送到内核。不仅如此,Shell有自己的编程语言用于对命令的编辑,它允许用户编写由shell命令组成的程序。Shell编程语言具有普通编程语言的很多特点,比如它也有循环结构和分支控制结构等,用这种编程语言编写的Shell程序与其他应用程序具有同样的效果。
处理流程:Linux系统上的所有可执行文件都可以作为Shell命令来执行。 当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;若是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。
6.3 Hello的fork进程创建过程
在shell中通过命令./hello就会创建一个新的进程。Bash进程通过调用fork函数来创建将要运行的新的hello的子进程。新创建的子进程几乎但不完全与父进程相同。子进程得到与父进程用户级虚拟地址空间相同的(但是独立的)一份副本,包括代码和数据段、堆、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中打开的任何文件。父进程和新创建的子进程之间最大的区别在于它们有不同的PID。父进程和子进程独立运行,二者顺序不可知。父进程负责回收子进程。Fork函数调用一次返回两次,可根据返回值区分父进程和子进程。
6.4 Hello的execve过程
Execve函数在当前进程的上下文中加载并运行一个新程序。Execve函数加载并运行可执行目标文件hello,且带参数列表argv和环境变量列表envp。除非出现错误,execve调用一次不返回。argv变量指向一个以NULL结尾的指针数组,其中每个指针都指向一个参数字符串。在execve加载了hello后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数。
6.5 Hello的进程执行
进程上下文信息:内核为每个进程维持一个上下文(context)。上下文就是内核重新启动一个被抢占的进程所需的状态。它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构,比如描述地址空间的页末、包含有关当前进程信息的进程表,以及包含进程已打开文件的信息的文件表。
时间片:一个进程执行它的控制流的一部分的每一时间段叫作时间片。
进程的调度:操作系统内核使用一种称为上下文切换(contextswitch)的较高层形式的异常控制流来实现多任务。在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程。这种决策就叫做调度(scheduling),是由内核中称为调度器(scheduler)的代码处理的。当内核选择一个新的进程运行时,我们说内核调度了这个进程。在内核调度了一个新的进程运行后,它就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程,上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
用户态与核心态的转换:运行应用程序代码的进程初始时是在用户模式中的。进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式。处理程序运行在内核模式中,当它返回到应用程序代码时,处理器就把模式从内核模式改回到用户模式。
6.6 hello的异常与信号处理
异常类型:
中断:来自I/O设备的信号
陷阱:有意的异常
故障:潜在可恢复的错误
终止:不可恢复的错误
可能产生的信号: SIGINT(来自键盘的中断)、SIGKILL(杀死程序)、SIGALRM(来自alarm函数的定时器信号)、SIGCHLD(一个子进程停止或者终止)、SIGTSTP(来自终端的停止信号)等。
- Ctrl+C时,程序收到SIGINT信号,处理方法:终止程序
当hello的进程终止后,bash会收到SIGCHLD信号,回收hello进程
- Ctrl+Z时,hello会接收到SIGTSTP信号,进程挂起
通过ps指令查看当前进程
由此可看出,hello仍然存在,未被回收
通过jobs查看停止的进程:
可以得出hello已停止的结论。
通过fg恢复hello前台运行
通过pstree看到进程树
我们可以找到hello在进程树中的位置
当前进程:
kill后进程
运行过程中乱按
乱按的第1个回车之前的所有内容会留给getchar(),剩下的所有内容留给随后hello结束之后的命令行输入,以回车为分隔,将输入理解成命令来执行。由于是乱输入的,因此基本上都是提示找不到命令。
6.7本章小结
本章先阐述了进程的概念和作用,shell-bash的作用和处理流程,fork创建进程和execve执行进程和hello的执行。最后分析了hello运行过程中可能遇到的异常及处理方法。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(以下格式自行编排,编辑时删除)
结合hello说明逻辑地址、线性地址、虚拟地址、物理地址的概念。
7.2 Intel逻辑地址到线性地址的变换-段式管理
(以下格式自行编排,编辑时删除)
7.3 Hello的线性地址到物理地址的变换-页式管理
(以下格式自行编排,编辑时删除)
7.4 TLB与四级页表支持下的VA到PA的变换
(以下格式自行编排,编辑时删除)
7.5 三级Cache支持下的物理内存访问
(以下格式自行编排,编辑时删除)
7.6 hello进程fork时的内存映射
(以下格式自行编排,编辑时删除)
7.7 hello进程execve时的内存映射
(以下格式自行编排,编辑时删除)
7.8 缺页故障与缺页中断处理
(以下格式自行编排,编辑时删除)
7.9动态存储分配管理
(以下格式自行编排,编辑时删除)
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
(以下格式自行编排,编辑时删除)
(第7章 2分)
第8章 hello的IO管理
8.1 Linux的IO设备管理方法
(以下格式自行编排,编辑时删除)
设备的模型化:文件
设备管理:unix io接口
8.2 简述Unix IO接口及其函数
(以下格式自行编排,编辑时删除)
8.3 printf的实现分析
(以下格式自行编排,编辑时删除)
[转]printf 函数实现的深入剖析 – Pianistx – 博客园
从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等.
字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。
显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。
8.4 getchar的实现分析
(以下格式自行编排,编辑时删除)
异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结
(以下格式自行编排,编辑时删除)
(第8章1分)
结论
hello的一生是精彩且神奇的!
- 程序员手动编写hello.c
- 预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。
- 编译器(ccl)将文本文件hello.i翻译成文本文件hello.s
- 汇编器(as)将hello.s翻译成机器语言指令,并打包在可重定位目标文件hello.o中
- 链接器(ld)将hello.o与各种库链接为可执行目标程序,保存在hello中
- 用户在shell中输入命令,运行hello
- Shell用fork和execve创建hello的子进程。
- CPU一步一步执行指令。
- 遇到异常时,调用异常处理程序,hello在用户和内核切换。
- hello 结束,shell回收子进程,hello彻底消失。
学习CSAPP是痛并快乐的过程,学习的过程非常痛苦,看不懂的书,难搞的实验和大作业。但通过这门课对计算机系统有了更深刻的认识。
附件
列出所有的中间产物的文件名,并予以说明起作用。
文件名
文件作用
hello.c
源程序文件
hello.i
预处理后文件
hello.s
编译后文件
hello.o
汇编后文件(可重定位目标文件)
hello
链接后文件(可执行目标文件)
参考文献
[1] 深入理解计算机系统 Randal E.Bryant David R.O’Hallaron
[2] 进程|菜鸟教程
[3] 动态链接的具体过程CSDN