哈工大计算机系统大作业 程序人生-Hello’s P2P From Program to Process
Posted Stella030
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈工大计算机系统大作业 程序人生-Hello’s P2P From Program to Process相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113643
班 级 2103103
学 生 林之彦
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
本文对Hello程序为例进行分析,研究一个程序从一个高级C语言程序开始,经过预处理、编译、汇编、链接等到最后变成一个可执行文件的生命周期,以此来了解系统如何通过硬件和系统软件的交织、共同协作以达到运行应用程序的最终目的。将全书内容融会贯通,帮助深入理解计算机系统。
关键词:计算机系统;预处理;编译;汇编;链接;进程管理;存储管理;I/0;
目 录
第1章 概述
1.1 Hello简介
Hello经历了所有计算机程序从诞生到运行的全过程:编写高级语言程序;预处理、编译、汇编、链接;fork创建子进程;execve加载并运行;受到系统的存储管理与I/O管理,最后结束运行,被内核、bash回收。理解了Hello的P2P,020的整个过程,那么就能对整个计算机系统的工作原理作出更深刻的理解。
1.1.1 P2P
P2P (From Program to Process),意为程序到进程。Hello的P2P过程如下:
(1)编辑hello.c文本。
(2)GCC编译器驱动程序完成从源文件到目标文件的转化:
(i) 预处理:cpp根据以字符#开头的命令,修改原始的C程序,得到hello.i;
(ii) 编译:ccl将hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,每条语句都以一种文本格式描述了一条低级机器语言指令;
(iii) 汇编:as将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序,并将结果保存在目标文件hello.o中,它是一个二进制文件;
(iv) 链接:ld将hello.o与printf.o合并,得到可执行目标文件Hello。
(3)在shell中启动,调用fork函数创建进程,调用execve函数加载并运行Hello,通过内存映射,分配空间等让Hello与其他进程并发进行,到这里Hello就顺利变成了进程。
1.1.2 020
020 (From Zero to Zero),意为从0到0。源程序从无到有被编写,所以是从0开始,然后编译生成可执行目标文件Hello,而后在运行的时候拥有了自己的进程,也在内存中存储了相关信息,进程终止之后被回收并释放,所以是以0结束。这就是Hello的020过程。
1.2 环境与工具
1.2.1硬件环境
CPU: 11th Gen Intel(R) Core(TM) i7-1165G7 @ 2.80GHz
RAM: 16GB RAM (15.8GB可用)
Disk: 512GB SSD
1.2.2软件环境
Windows 10; VirtualBox11; Ubuntu 16.04 LTS 64
1.2.3开发工具
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc
1.3 中间结果
文件名称 | 文件说明 |
hello.i | 预处理生成的文本文件 |
hello.s | .i文件编译后得到的汇编语言文件 |
hello.o | .s文件汇编后得到的可重定位目标文件 |
hello | .o经过链接生成的可执行目标文件 |
elf.txt | hello.o的elf文件 |
asm_hello.s | hello.o的反汇编代码文件 |
hello.elf | hello的elf文件 |
1.4 本章小结
本章介绍了P2P和020的过程,以及为编写本论文使用的软硬件环境和开发与调试工具,同时列出了本实验得到的中间结果文件及其作用。
第2章 预处理
2.1 预处理的概念与作用
预处理器根据以字符#开头的命令,修改原始的C程序。如hello.c中第一行的#include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。
预处理的结果得到了另一个C程序,通常以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
Linux下预处理命令:
(1)gcc -E -o hello.i hello.c
(2)cpp hello.c hello.i
输入预处理命令后,我们得到了如图所示的已完成预处理的文件:
图2-1 预处理过程
2.3 Hello的预处理结果解析
修改得到的C程序hello.i从hello.c的23行增加到3105行,同时发现main函数在文件的最后部分。预处理对头文件和宏定义进行了操作,所有的#include等语句全部被替换,取而代之的是一些路径以及用到的相关语句。并且预处理同时也删除了所有的注释信息。
2.4 本章小结
本章介绍了预处理的概念、作用以及实现方式。查看并分析了hello.c在Ubuntu下预处理的结果。
第3章 编译
3.1 编译的概念与作用
编译器将后缀为.i的文本文件翻译成后缀为.s的文本文件,它包含一个汇编语言程序,其中的每条语句都以一种文本格式描述了一条低级机器语言指令。
汇编语言是非常有用的,因为它为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令
Linux下编译命令:
(1)gcc -S hello.c -o hello.s
(2)ccl hello.i -o hello.s
输入编译命令后,我们得到了如图所示的汇编语言文件:
图3-1 编译过程
3.3 Hello的编译结果解析
3.3.1数据
Hello的后面是两个%s类型的字符串,对应着我们输入的前两个变量:学号和姓名。
图3-2 printf语句中的格式串
分析汇编代码,能够看出循环变量i所存储的位置在-4(%rbp)处。
图3-3 循环变量i声明及变化
argc被存储在-20(%rbp)的位置,argv被存储在-32(%rbp)的位置。
图3-4 变量argc和argv的存储
3.3.2赋值
根据上文,结合这段汇编代码可知把存储在-4(%rbp)位置的i赋值为0。其中movl表示操作的对象是四个字节。
图3-5 给变量i赋值
3.3.3类型转换
调用函数atoi,其参数为argv[3],这个函数将字符串转换为整型。
图3-6 类型转换
3.3.4算术操作
这条汇编代码将变量i的值+1.
图3-7 算术操作
3.3.5关系操作
地址-20(%rbp)处存储的是argc的值,将argc的值与4进行比较,如果相等则跳转到L2继续执行。
图3-8 关系操作1
地址-4(%rbp)处存储的是i,将i的值与8进行比较,若比较结果为小于等于则跳转到L4,继续执行循环体,若大于则跳出循环。
图3-9 关系操作2
3.3.8数组/指针操作
数组argv存储在地址-32(%rbp)处。
图3-10 数组的存储
然后通过指针访问argv[1], argv[2]和argv[3]的存储地址,将字符串的值存储在寄存器中,作为参数传递给函数printf和atoi。
图 3-11 指针寻址访问数组
3.3.9控制转移
- if语句
判断argc是否等于4,若相等则跳转,执行后续操作,若不等则继续执行下一条语句。
图3-12 if语句
- for循环语句
先将变量i赋初值0,然后跳转到L3,将i的值与8进行比较,如果i小于等于8则执行循环体,单次循环结束后i++,重复上述操作直到i大于8跳出循环。
图3-13 for循环语句
3.3.10函数操作
main函数
传参为argc和argv,分别存储在-20(%rbp)和-32(%rbp)中。返回值存储在%eax中。
puts函数
取出.LC0(%rip)中的字符串存入%rdi中,作为参数传递给puts。
图3-14 puts函数
printf函数
将argv[1]和argv[2]的值取出,存储在%rsi和%rdx中,取出.LC1(%rip)处的字符串存入%rsi中,作为参数传递给printf。
图3-15 printf函数
exit函数
将立即数1赋值给%edi,作为参数传递给exit。(exit(1))
图3-16 exit函数
- atoi函数
通过指针将argv[3]的值存入%rax中,再将%rax赋给%rdi,参数通过%rdi进行传递,返回值存储在%eax中。
图3-17 atoi函数
- sleep函数
将%eax中存储的atoi函数的返回值赋给%edi,参数通过%edi进行传递,通过call指令调用。
图3-18 sleep函数
- getchar函数
通过call指令直接调用,无需传参。
图3-19 getchar函数
3.4 本章小结
本章讲述了编译的概念和作用,并结合hello.c的编译结果对于汇编语言中的各部分以及各种操作进行了详细的说明。在编译完成的汇编语言程序中,我们可以看到汇编代码对内存以及寄存器的各种底层操作。理解汇编语言和编译器等细节是理解更深和更基本概念的先决条件。
第4章 汇编
4.1 汇编的概念与作用
汇编器将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
4.2 在Ubuntu下汇编的命令
Linux下汇编命令:
(1)gcc hello.s -c -o hello.o
(2)as hello.s -o hello.o
图4-1 汇编过程
4.3 可重定位目标elf格式
图4-2 通过命令导出elf文件
4.3.1 ELF头
图4-3 ELF头
ELF头以一个16字节的序列开始,这个序列是对于声称该文件系统下一些字的大小等信息的描述。而后包含一些能够帮助链接器语法分析并解释目标文件的信息,包括ELF头的大小、目标文件的版本、机器类型、节头部表的文件偏移、以及节头部表中条目的大小和数量。
4.3.2节头表
图4-4 节头表
节头表描述了.o文件中每一个节出现的位置、大小,目标文件中的每一个节都有一个固定大小的条目。
4.3.3重定位节
图4-5 重定位节
重定位节包含了在代码中使用的一些外部变量等信息,在链接的时候需要根据重定位节的信息对于某些变量符号进行修改。链接的时候链接器会根据重定位节的信息对于外部变量符号决定选择何种方法计算正确的地址,例如通过偏移量等信息计算。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,atoi, sleep,getchar。
4.3.4符号表
图4-6 符号表
symtab是一个符号表,它存放于程序中定义和引用的函数和全局变量的信息。例如本程序中的getchar、puts、exit等函数。
4.4 Hello.o的结果解析
从hello.o的反汇编中可以看出,机器语言中每个不同的指令,即使操作数不同,长度也是固定的,相同的指令具有相同的前缀。依照这个原则,如果有了机器码和汇编指令的对照表,就可以完成汇编语言与机器语言的互相翻译。
(a)hello.s (b)asm_hello.s
图4-7 hello.s与hello.o的反汇编结果对照
(1)分支转移:反汇编代码跳转指令的操作数由段名称变成了确定的地址。
(2)函数调用:在hello.s中,函数调用之后直接跟着函数名称,而在反汇编程序中,call的目标地址是当前下一条指令。这是因为hello.c中调用的函数都是共享库中的函数,最终需要通过动态链接器才能确定函数的运行时执行地址。
(3)数的表示:hello.s里的数是十进制表示,asm_hello.s里的数是十六进制表示。
(4)全局变量访问:hello.s中直接通过段名称+%rip访问rodata,但是在asm_hello.s中,由于不知道rodata的数据地址,所以只能先写成0+%rip进行访问,再在后续的操作中利用重定位和链接来访问rodata。
4.5 本章小结
本章介绍了hello从hello.s到hello.o的汇编过程,比较了前后的的结果,了解了从汇编语言映射到机器语言汇编器需要实现的转换。
第5章 链接
5.1 链接的概念与作用
链接是将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。链接可以执行于编译时、加载时或运行时。
在现代系统中,链接是由叫做链接器的程序自动执行的。链接器使得分离编译成为可能,无需将大型的应用程序组织成为一个巨大的源文件,而是可以把它分解为更小、更好管理的模块,可以独立地修改和编译这些模块。当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。
5.2 在Ubuntu下链接的命令
Linux下链接命令:
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-1 链接过程
5.3 可执行目标文件hello的格式
5.3.1 elf头
hello.elf的elf头和elf.txt的elf头所包含的信息种类基本相同,不同的是程序头大小和节头数量都得到了增加,并且获得了入口地址。
图5-2 elf头
5.3.2节头表
相较于elf.txt,内容更加丰富详细。当完成链接之后程序中的一些文件就被添加进来了,每一节都有了实际地址。
图5-3 节头表
5.3.3 重定位节
可以发现hello.elf的重定位节与elf.txt的重定位节的名字以及内容都完全不一样,现在的所有加数都是0,证明在链接环节确实完成了各种重定位效果。
图5-4 重定位节
5.3.4. 符号表
与elf.txt相同的是,符号表的功能没有发生变化,所有重定位需要引用的符号都在其中说明;不同的是,main函数以外的符号也拥有了type,这证明完成了链接。
图5-5 符号表
5.3.5. 程序头
程序头是一个结构数组,描述了系统准备程序执行所需的段或者其他信息。程序头描述了可执行目标文件的连续的片与连续的虚拟内存段之间的映射关系。从程序头中可以看到根据可执行目标文件的内容初始化为了两个内存段,分别为只读内存段(代码段)和读写代码段(数据段)。
图5-6 程序头
5.4 hello的虚拟地址空间
用edb打开hello,由data dump部分可以看出,程序是从0x400000开始加载的,结束在约0x400ff0位置。
图5-7 hello的虚拟地址空间
然后查看.elf 中的程序头部分。程序头表在执行的时候被使用,它告诉链接器运行时加载的内容并提供动态链接的信息。每一个表项提供了各段在虚拟地址空间和物理地址空间的大小、位置、标志、访问权限和对齐等信息。各表项功能如下:
PHDR:程序头表
INTERP:程序执行前需要调用的解释器
LOAD:保存常量数据、程序目标代码等
DYNAMIC:保存动态链接器使用信息
NOTE:保存辅助信息
GNU_STACK:异常标记
GNU_RELRO:保存重定位后只读区域的位置
5.5 链接的重定位过程分析
重定位指的是链接器在完成符号解析以后,就把代码中的每个符号引用和正好一个符号定义关联起来。此时,链接器就知道它的输入目标模块中的代码节和数据节的确切大小。然后就可以开始重定位步骤了,在这个步骤中,将合并输入模块,并为每个符号分配运行时的地址。然后是重定位节中的符号引用,链接器会修改hello中的代码节和数据节中对每一个符号的引用,使得它们指向正确的运行地址。
可以看到这次的汇编代码中call时的地址都变为了绝对地址,不再是最初的函数名字或相对地址。而且多了很多通过链接加进来的函数的源代码,如printf等。
图5-8 hello的反汇编代码
5.6 hello的执行流程
名称 | 地址 |
_init | 0x4004c0 |
.plt | 0x4004e0 |
puts@plt | 0x4004f0 |
printf@plt | 0x400500 |
getchar@plt | 0x400510 |
atoi@plt | 0x400520 |
exit@plt | 0x400530 |
sleep@plt | 0x400540 |
_start | 0x400550 |
main | 0x400582 |
__libc_csu_init | 0x400610 |
__libc_csu_fini | 0x400680 |
_fini | 0x400684 |
5.7 Hello的动态链接分析
对于动态共享链接库中PIC函数,编译器没有办法预测函数的运行时地址,所以需要添加重定位记录,等待动态链接器处理,为避免运行时修改调用模块的代码段,链接器采用延迟绑定的策略。动态链接器使用过程链接表PLT+全局偏移量表 GOT实现函数的动态链接,GOT中存放函数目标地址,PLT使用GOT中地址跳转到目标函数。
5.8 本章小结
本章介绍了链接的概念与作用、hello的ELF格式,通过使用edb以及阅读文件elf,可以看到程序的执行过程以及其中涉及到的程序地址动态链接内容与相关函数调用。分析了hello的重定位过程、执行流程、动态链接过程。
第6章 hello进程管理
6.1 进程的概念与作用
进程的概念:
一个执行中程序的实例,系统中的每个程序都运行在某个进程的上下文之中。上下文是由程序正确运行所需的状态组成的。这种状态包括存放在内存中的程序的代码以及数据、栈、通用目的寄存器的内容、程序计数器、环境变量以及打开文件描述符的集合。
进程的作用:
进程能够提供给应用程序的一些关键抽象:
(1)一个独立的逻辑控制流,它提供一个假象,好像程序能够独占使用处理器。
(2)一个私有的地址空间,它提供一个假象,好像程序能够独占使用内存系统。
6.2 简述壳Shell-bash的作用与处理流程
Shell是一个命令行解释器,它输出一个提示符,等待输入一个命令行,然后执行这个命令。如果该命令行的第一个单词不是一个内置的Shell命令,那么Shell就会假设这是一个可执行文件的名字,它将加载并运行这个文件。在运行Hello时,Shell将加载并运行Hello程序,然后等待程序终止。程序终止后,Shell随后输出一个提示符,等待下一个输入的命令行。
6.3 Hello的fork进程创建过程
pid_t fork(void);
进程通过调用fork函数创建一个新的运行的子进程。子进程得到与父进程用户级虚拟地址空间相同但独立的一份副本,包括代码、数据段、堆、共享库以及用户栈,子进程获得与父进程任何打开文件描述符相同的副本,这就意味着当父进程调用fork时,子进程可以读写父进程中的内容,但它们有着不同的PID,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4 Hello的execve过程
int execve(const *filename, const char *argv[], const char *envp[]);
execve过程发生在调用fork创建新的子进程之后。作用是在当前进程的上下文中加载并运行一个新的程序。filename是可执行目标文件,argv是参数列表,envp是环境变量列表。它调用一次,从不返回,只有出现错误时execve才会返回到调用程序。
6.5 Hello的进程执行
内核为每个进程维持一个上下文,它由一些对象的值组成,这些对象包括通用目的寄存器、浮点寄存器、程序计数器、用户栈、状态寄存器、内核栈和各种内核数据结构。
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度。在内核调度了一个新的进程运行后,他就抢占当前进程,并使用一种称为上下文切换的机制来将控制转移到新的进程。上下文切换1)保存当前进程的上下文,2)恢复某个先前被抢占的进程被保存的上下文,3)将控制传递给这个新恢复的进程。
一个进程执行它的控制流的一部分的每一时间段叫做进程时间片。
进程从用户模式变为内核模式的唯一方法是通过诸如中断、故障或者陷入系统调用这样的异常。当异常发生时,控制传递到异常处理程序,处理器将模式从用户模式变为内核模式,处理程序运行在内核模式中,当它返回到应用程序代码是,处理器就把模式从内核模式改回到用户模式。
图6-1 进程上下文切换的剖析
6.6 hello的异常与信号处理
脸滚键盘(输入回车、随机字符串)
图6-2 输入回车、随机字符串
可以发现回车和随机字符串对于程序的运行并没有影响,终端中会出现所有相关的输入,并不会影响到现有程序运行。
Ctrl-C:
图6-3 输入Ctrl-C
当向hello程序输入Ctrl-C后,会导致中断异常产生SIGINT信号,向子进程发出SIGKILL信号终止并回收,进程终止。
Ctrl-Z:
图6-4 输入Ctrl-Z
当向hello程序输入Ctrl-Z后,会导致中断异常产生SIGSTP信号,进程被挂起,与输入Ctrl-C结果不同。
此时分别输入ps, jobs, pstree, fg, kill指令进行查看相关信息。
ps:
图6-5 输入ps
可以看出hello进程并未停止,而是被挂起。
jobs:
图6-6 输入jobs
验证了hello进程确实被挂起,处于停止的状态。
pstree:
图6-7 输入pstree
可以通过进程树来查看所有进程的情况。
fg:
图6-8 输入fg
fg指令使第一个后台作业变成前台作业,这里hello是第一个后台作业,所以fg会使得hello回到前台并完成运行。
kill:
图6-9 输入kill
结合ps指令输出的进程列表以及提示信息可知,kill指令成功杀死进程hello。
6.7本章小结
本章简述了进程的概念与作用,以及shell的执行流程,总结了fork和execve的运行过程,以及在上下文切换中用户态和内核态的切换,探究了在进程运行过程中信号的作用。
第7章 hello的存储管理
7.1 hello的存储器地址空间
(1)逻辑地址(logical address)
包含在机器语言指令中用来指定一个操作数或一条指令的地址。它促使程序员把程序分成若干段。每一个逻辑地址都由一个段(segment)和偏移量(offset)组成,偏移量指明了从段开始的地方到实际地址之间的距离。对应于hello.o中的相对偏移地址。
- 线性地址(Linear Address)
逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。如果启用了分页机制,那么线性地址能再经变换以产生一个物理地
哈工大计算机系统大作业——程序人生-Hello’s P2P
本篇论文将从计算机系统课程中的所学内容通过hello小程序的一生,即从hello.c程序编写完成到hello预处理、编译、汇编、链接最后被回收结束该过程,来对所学知识内容进行全面的梳理与回顾。我主要在Linux系统下的Ubuntu20.04操作系统中进行内存管理、进程管理、I/O管理、虚拟内存以及异常信号等相关操作,在合理运用了Ubuntu下的一些操作工具,并且进行了细致的历程分析。通过勾勒出hello完整坎坷却不失华丽的一生,目的是为了加深自己对计算机系统的理解,同时从计算机系统层面上在各个方面游览了hello程序的生命周期,揭开了它的神秘面纱。
关键词:预处理;Linux;020;P2P;Ubuntu;汇编;链接;计算机系统
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
1.1.1 P2P
Program:在编译中键入敲出代码得到hello.c程序时,我们敲击键盘的过程中,字符被读入寄存器,然后被存入内存中。当我们保存文件并退出,程序文本被交换到了磁盘里。
Process: hello.c在Linux系统中运行shell程序输入命令gcc hello.c -o hello后经过cpp的预处理、ccl的编译、as的汇编以及ld的链接最终成为了可执目标程序文件hello。继而在shell中键入启动命令后,shell为其创建子进程(fork),产生子进程,接着把hello的内容加载到子进程的地址空间,当子进程执行完return语句后,它保持已终止状态,此时向shell发送sigchild信号,等待shell对其进行回收。当shell调用waitpid指示操作系统将其回收后,hello的生命周期便结束了。
1.1.2 020
shell为hello进程execve来映射虚拟内存,进入程序入口后程序才开始载入物理内存。进入main()函数执行其目标代码, CPU则为运行的hello分配时间片从而执行逻辑控制流。当hello程序运行结束后,shell的父进程便负责回收hello进程,最后内核删除相关数据结构。
1.2 环境与工具
1.2.1硬件环境
处理器:12th Gen Intel(R) Core(TM) i7-12700H 2.70 GHz
RAM:16.00GB
系统类型:64位操作系统,基于x64的处理器
1.2.2软件环境
Windows11 64位;Ubuntu 20.04
1.2.3开发与调试工具
gcc,as,ld,vim,edb,readelf,visual studio
1.3 中间结果
文件的名字 | |
经过预处理后的hello源程序文本文件 | hello.i |
经过编译之后产生的汇编文件文本文件 | hello.s |
经过汇编之后产生的可重定位目标文件二进制文件 | hello.o |
通过链接产生的可执行目标文件二进制文件 | hello |
hello.o的ELF格式文本文件 | elf.txt |
hello.o的反汇编代码文本文件 | Disas_hello.s |
hello的ELF格式文本文件 | hello1.elf |
hello的反汇编代码文本文件 | hello1_objdump.s |
1.4 本章小结
本章是对hello程序进行了一个总体详细而简明的概括,首先介绍P2P、020的步骤过程及其意义,然后又介绍大作业中我所需使用的硬件环境、软件环境和开发调试工具,最后则简述从hello.c文件到hello可执行文件所见所经历的过程中产生的中间结果。众所周知Hello World程序基本上是所有的程序员在学习第一门程序语言时所编写的第一个程序,接下来我们将继续探究该程序的一生,从各个方面了解和理解其执行细节。
(第1章0.5分)
第2章 预处理
2.1 预处理的概念与作用
2.1.1预处理的概念:
预处理(preprocessing)指的是预处理器(cpp)根据以字符#开头的命令,来修改原始的C程序,最后生成.i文本文件的过程。预处理中首先会展开以#起始的行,试图解释为预处理指令(preprocessing directive) ,其中关于ISO C/C++要求支持的包括有#if、#ifdef、#ifndef、#else、#elif、#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
2.1.2预处理的作用:
- 将源文件中用#include 形式声明的文件复制到新的程序中。例如hello.c文件中第6-8行的#include<stdio.h>等命令使得预处理器读取系统头文件stdio.h、unistd.h和stdlib.h的内容,并将其直接插入到程序文本中;
- 用实际值替换用#define 定义的字符串;
- 根据#if后面的条件决定需要编译的代码;
- 预编译程序可以识别一些特殊符号,预编译程序对于在源程序中出现的这些特殊符号串会用合适值进行替换。
- 预处理还可以帮助程序员节省工作量,提高程序可读性,便于维护。
2.2在Ubuntu下预处理的命令
命令:cpp hello.c > hello.i
图1 Ubuntu下预处理命令
2.3 Hello的预处理结果解析
此时我发现程序共拓展成了3060行,原始hello.c程序出现在3046行之后。这之前是头文件stdio.h、unistd.h以及stdlib.h依次展开。其中以stdio.h展开为例: stdio.h是标准库文件,预处理cpp需要在Ubuntu中默认的环境变量下寻找stdio.h,打开文件/usr/include/stdio.h,可以发现其中依然使用了#define宏定义语句,cpp对stdio中的宏定义是递归展开的,所以最终的.i文件中是没有#define的,但是其中却使用了大量的#ifdef 以及#ifndef 条件编译语句,cpp会对条件值进行判断来决定是否执行包含在其中的条件逻辑。同时之前提到过,预编译程序可识别一些特殊符号,预编译程序对于在源程序中出现的这些特殊符号串会用合适值进行替换。
hello.i插入文件库所在位置:
图2 hello.i库文件部分
hello.i库中预置声明函数位置:
图3 hello.i库中预置声明函数部分
hello.i源代码位置:
图4 hello.i源代码部分
2.4 本章小结
本章主要是对预处理(包括头文件展开、宏替换、去掉注释、条件编译等等)的概念和作用进行介绍,除此之外还有Ubuntu下预处理的两个指令,同时也具体到了我的hello.c文件预处理结果hello.i文本文件的详细解析,深刻理解了预处理的重要内涵。我知晓了预处理可以最大程度上减轻程序员的负担,提供一定的可移植性:通过宏定义(#define)可以更简明地定义一些实用“函数”,再通过各种编译命令,将所写代码根据不同运行的平台进行调整,从而避免不必要麻烦。
(第2章0.5分)
第3章 编译
3.1 编译的概念与作用
3.1.2编译的概念:
编译程序是通过词法分析和语法分析,在确认所有的指令都符合语法规则后,将其翻译成等价的中间代码或汇编代码来表示。编译器(cc1)将预处理过的文本文件hello.i 翻译转换成汇编文本文件hello.s。
3.1.2编译的作用:
以下为编译基本流程:
1.语法分析:编译程序的语法分析器以单词符号作为输入,分析单词符号串是否形成了符合语法规则的语法单位,方法有自上而下分析法和自下而上分析法两种;
2.中间代码:源程序的一种内部表示,或称之为中间语言。中间代码的作用是使编译程序的结构在逻辑上更为简单明确,特别是使目标代码的优化比较容易实现;
3.代码优化:指对程序进行多种等价变换,使得从变换后的程序出发时能生成更有效的目标代码;
4.目标代码:生成目标代码是编译的最后阶段。目标代码生成器把语法分析或优化后生成的中间代码转换成目标代码。此处目标代码指汇编语言代码,即不同种类的语言提供相同的形式,其指令与处理器的指令集类似,更贴近底层,便于汇编器将其转换为可执行机器语言代码供机器执行。
3.2 在Ubuntu下编译的命令
命令:gcc -S hello.i -o hello.s
图5 Ubuntu下编译命令
3.3 Hello的编译结果解析
3.3.1.1常量
在if语句if (argc != 4)中,常量4的值保存的位置在.text中,作为指令的一部分:movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
同理可得循环:
for (i = 0; i < 8; i++)
printf("Hello %s %s\\n", argv[1], argv[2]);
sleep(atoi(argv[3]));
中的数字0、8、1、2、3也是被存储在.text中;
在下述函数中:
printf("用法: Hello 7203610121 刘天瑞 599184000s!\\n");
printf()中的字符串则被存储在.rodata中:
.LC0:
.string "\\323\\303\\267\\250: Hello 7203610121 \\301\\365\\314\\354\\310\\360 599184000s\\243\\241"
3.3.1.2变量
全局变量:
初始化全局变量储存在.data中,其初始化不需要汇编语句而可以直接完成。
局部变量:
局部变量存储在寄存器或栈中,程序中局部变量由i定义:
int i;
在汇编代码中:
.L2:
movl $0, -4(%rbp)
jmp .L3
此处是循环前i=0的操作,i被保存在栈当中的%rsp-4位置上。
3.3.2 算术操作
在循环操作中使用自加++操作符:for (i = 0; i < 8; i++)
即在每次循环执行的内容结束后,对i进行一次自加,栈上存储变量i的值加1: addl $1, -4(%rbp)
3.3.3 控制转移/关系操作
程序第13行中判断传入参数argc是否等于4,源代码为:
if (argc != 4)
printf("用法: Hello 7203610121 刘天瑞 599184000s!\\n");
exit(1);
汇编代码为:
cmpl $4, -20(%rbp)
je .L2
其中je判断cmpl是否产生条件码,如果两个操作数的值不相等则跳转至指定地址;
for循环中的循环执行条件:for (i = 0; i < 8; i++)
汇编代码为:
.L3:
cmpl $7, -4(%rbp)
jle .L4
其中jle判断cmpl是否产生条件码,如果后一个操作数的值小于或等于前一个操作数的值则跳转至指定地址。
3.3.4 数组/指针/结构操作
主函数main()的参数中存在指针数组char *argv[]:
int main(int argc, char* argv[])
在argv[]数组中,易知argv[0]指向输入程序数组的路径和名称,argv[1]和argv[2]则分别表示两个字符串。char* 数据类型占8个字节:
.LFB6:
movl %edi, -20(%rbp)//argc存储在%edi
movq %rsi, -32(%rbp)//argv存储在%rsi
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
.L4:
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
.LC1:
.string "Hello %s %s\\n"
.text
.globl main
.type main, @function
通过对比原函数可知,%rsi-8和%rax-16分别得到了argv[1]和argv[2]两个字符串。
3.3.5 函数操作
在x86-64中有过程调用传递参数的规则如下:
1~6参数依次存储在%rdi、%rsi、%rdx、%rcx、%r8、%r9六个寄存器中,剩下的参数则可以直接存储在栈当中。
下面介绍各类函数:
main函数:
参数传递:输入参数argc和argv[],分别存储在寄存器%rdi和%rsi中;
函数调用:被系统启动的函数调用;
函数返回:设置%eax=0并返回,对应为:return 0 ;
源代码如下:
int main(int argc, char* argv[])
汇编代码如下:
main:
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
可以发现argc存储在%edi中,存储在%rsi中;
printf函数:
参数传递:call puts时只输入字符串参数首地址,for循环中call printf时输入 argv[1]和argc[2]的地址;
函数调用:if判断满足条件后在for循环中被调用;
源代码1如下:
printf("用法: Hello 7203610121 刘天瑞 599184000s!\\n")
汇编代码1如下:
.LC0:
.string "\\323\\303\\267\\250: Hello 7203610121 \\301\\365\\314\\354\\310\\360 599184000s\\243\\241"
.LFB6:
.cfi_startproc
endbr64
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
cmpl $4, -20(%rbp)
je .L2
leaq .LC0(%rip), %rdi
call puts@PLT
movl $1, %edi
call exit@PLT
源代码2如下:
printf("Hello %s %s\\n", argv[1], argv[2])
汇编代码2如下:
.L4:
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
leaq .LC1(%rip), %rdi
movl $0, %eax
call printf@PLT
exit函数:
参数传递:输入参数为1后再执行退出命令;
函数调用:if判断条件满足后被调用;
源代码如下:exit(1);
汇编代码如下:
.LFB6:
movl $1, %edi
call exit@PLT
sleep函数:
参数传递:输入参数atoi(argv[3]),
函数调用:在for循环中被调用,call sleep;
源代码如下:sleep(atoi(argv[3]));
汇编代码如下:
.L4:
movq -32(%rbp), %rax
addq $24, %rax
movq (%rax), %rax
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
其中函数atoi()(表示为ascii to integer)是把字符串转换成整型数的一个函数,多应用在计算机程序和办公软件中。例如int atoi(const char *nptr) 函数会扫描参数 nptr字符串,跳过前面的空白字符(例如空格,tab缩进)等。如果 nptr不能转换成 int 或者 nptr为空字符串,那么将返回0。特别注意该函数要求被转换的字符串是按十进制数理解的。
getchar函数:
函数调用:在main中被调用,call getchar
源代码如下:getchar();
汇编代码如下:
.L3:
cmpl $7, -4(%rbp)
jle .L4
call getchar@PLT
3.4 本章小结
本章主要是对编译概念和作用进行详细介绍,同时也通过示例函数表明c语言是如何转换成为汇编代码的。除此之外我还介绍了汇编代码是如何实现变量、常量、传递参数、分支以及循环。编译程序所做的工作就是通过词法分析和语法分析,在确认所有的指令都符合语法规则后,将其翻译成等价的中间代码或汇编代码来表示。包括之前对编译结果进行详尽仔细地解析,都使得我更加深刻理解C语言数据与操作,对C语言编译成汇编语言有更为恰当的掌握。因为汇编语言具有通用性,我掌握了它也相当于掌握了语言间的一些共性。汇编是C语言源程序经过预处理器、编译器处理后的结果,也是最贴近机器底层的语言。即使在高级语言盛行的今天,仍然需要学习汇编并掌握它。最后,通过学习汇编可以理解编译器的优化性能,并且分析解析出代码中所隐含的低效率从而更好地优化程序。
(第3章2分)
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
驱动程序运行汇编器将汇编语言(hello.s)翻译成机器语言(hello.o)的过程称为汇编,同时这个机器语言文件是可重定位目标文件。汇编器接受.s文件作为输入,以.o可重定位目标文件作为输出。可重定位目标文件包含二进制代码和数据,在编译时与其他可重定位目标文件能够合并起来,创建成一个可执行目标文件,从而被加载到内存中执行。
4.1.2 汇编的作用
汇编是将高级语言转化为机器可以直接识别并且执行的代码文件的过程,汇编器将.s文件汇编程序翻译成机器语言指令,并将这些指令打包成为可重定位目标程序的格式,最后将结果保留在.o目标文件中,.o文件是一个包含程序指令编码的二进制文件。
4.2 在Ubuntu下汇编的命令
命令:as hello.s -o hello.o
图6 Ubuntu下汇编命令
4.3 可重定位目标elf格式
4.3.1 导出elf文件命令
命令: readelf -a hello.o > ./elf.txt
图7 Ubuntu下生成并导出elf文件命令
4.3.2 ELF头
其中包含了系统信息,编码方式,ELF头大小,节的大小和数量等一系列必要有效信息。描述生成该文件系统字节大小和字节顺序、帮助链接器语法分析以及解释目标文件的有效信息。 ELF头的主要内容如下所示:
ELF 头:
以上是关于哈工大计算机系统大作业 程序人生-Hello’s P2P From Program to Process的主要内容,如果未能解决你的问题,请参考以下文章