计算机系统大作业 程序人生-Hello’s P2P

摘  要

本文旨在以hello.c为例分析Linux环境下程序从生成到结束的过程,使用计算机系统相关知识从预处理、编译、汇编、链接、进程管理、储存管理以及IO管理等方面了解hello程序的一生,加深对于计算机系统的理解,感悟计算机设计的精妙。

 

关键词:环境与工具;预处理;编译;汇编;链接;进程管理;储存管理;IO管理                           

 

 

 

 

 

 

 

 

目  录

 

 

 

第1章 概述

1.1 Hello简介

P2P,即From Program to Process,从项目到进程,指的是hello.c先由预处理器cpp生成hello.i,然后通过编译器ccl生成hello.s,接着经过汇编器as生成可重定位目标程序hello.o,最后通过链接器ld链接生成可执行文件hello,最终在Linux终端运行可执行程序。

020,即From Zero-0 to Zero-0,指的是shell调用fork函数创建子进程,然后exceve,映射虚拟内存,并加载对应的物理内存,当该进程的时间片到达时,进程进入CPU流水线中执行。执行结束后,父进程回收子进程,内核删除相关数据结构,释放资源。

1.2 环境与工具

硬件环境:X64 CPU;2GHz;2G RAM;256GHD Disk 以上

软件环境:Windows7 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位

开发与调试工具:Visual Studio 2019 64位;GDB/OBJDUMP;GCC;EDB等

1.3 中间结果

列出你为编写本论文,生成的中间结果文件的名字,文件的作用等。

名称

作用

hello.c

程序源代码

hello.i

hello.c经过预处理器cpp生成的文件

hello.s

hello.i经过过编译器ccl生成的文件

hello.o

hello.s经过汇编器as生成可重定位目标程序

hello

hello.o经过链接器ld链接生成可执行文件hello

hello.out

hello反汇编后的可重定位文件

hello_o_elf.txt

hello.o的elf文件

hello_o.s

hello.o的反汇编文件

hello_elf.txt

hello的elf文件

hello_.s

hello的反汇编文件

 

1.4 本章小结

本章简单介绍了hello程序P2P和020的一生,列出了本实验的硬件环境、软件环境、开发与调试工具和本实验中生成的中间结果文件的名字和作用。

 

 

第2章 预处理

2.1 预处理的概念与作用

预处理的概念:在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。

预处理的作用:预处理器(cpp) 根据预处理指令,修改原始的C程序。

 

 

 

 

 

2.2在Ubuntu下预处理的命令

 

如图所示,


命令行为gcc -E hello.c -o hello.i

2.3 Hello的预处理结果解析

 

 

hello.i一共3000多行,与hello.c一样是C语言程序,比hello.c多的部分大部分是对头文件的展开。其中,第13行至33行是复制的头文件信息。此外,hello.c中的注释被删除。

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

2.4 本章小结

本章介绍了预处理的概念与作用,示范了在ubuntu中对C语言源程序进行预处理,并分析了预处理生成文件的主要内容。

第3章 编译

3.1 编译的概念与作用

编译的概念:把hello.i转化为hello.s,即预处理后的文件到生成汇编语言程序。

编译的作用:把高级语言(本文中为C语言)翻译成2进制语言。

       

 

3.2 在Ubuntu下编译的命令

如图所示,gcc


 -S hello.i -o hello.s

3.3 Hello的编译结果解析

3.3.1 数据

1.局部变量


在hello.c的第11行定义了一个int型局部变量i,在hello.s中,i存在了栈上-4(%rbp)。

2.字符串

在hello.c的第14行和18行有字符串,在hello.s中,它存储在.section..rodata只读数据段中,.string用于声明一个字符串。字符串数组的起始地址将作为参数传给
printf函数,用于字符串的输出。

 

3.数组


hello.c中存在参数argc存,在hello.s中,它存储在在寄存器%edi中。

 

 

3.3.2 赋值操作

1.不赋初值

hello.c中的int i,在hello.s的汇编语言中没有体现,直到使用并赋初值时才用寄存器来存储。

2.赋值


hello.c中的i =1,在hello.s的汇编语言中的表达如图所示,使用movl赋值因为i为int类型变量,占4个字节。

 

 

3.3.3算术操作

hello.c中的for循环中的i++,在hello.s中的汇编语言的表达如图所示。

 

 

3.3.4关系操作

1.argc!=4

hello.c中argc!=4部分,在hello.s中的汇编语言中的表达如图所示。

 

2. i<8


hello.c中i<8部分,在hello.s中的汇编语言中的表达如图所示。此处,汇编语言为判断i<=7,与C语言程序有所不同。

 

 

3.3.5 数组/指针/结构操作


hello.c中的指针数组char *argv[],在hello.s中存放在-16(%rbp)和-24(%rbp)中,利用栈指针的偏移量来读取,如图所示。

 

3.3.6控制转移

利用jmp指令等指令来进行转移控制,本程序的部分控制转移如图所示。

 

3.3.7函数操作

hello.s中,函数将需要的参数存放在寄存器中或栈中来进行参数传递,使用call指令来调用函数,返回值保存在寄存器中。本程序的实例如下。

1.     


printf

 

 

 

 

 

 

 


2.exit

 

 

3.getchar

 

 

 

 

4.sleep

 

 

 

5.atoi

 

atoi函数可以把字符串转换成整型数。

 

3.4 本章小结

本章简要介绍了编译的概念和编译的作用,然后在Ubuntu中把hello.i编译为hello.s。重点分析了hello.s中数据、赋值、算术操作、关系操作、控制转移、数组/指针/结构操作、控制转移和函数操作的实现过程。

第4章 汇编

4.1 汇编的概念与作用

汇编的概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成可重定位目标程序的格式,并将结果保存在hello.o目标文件中。

汇编的作用:把汇编语言翻译成机器语言。

4.2 在Ubuntu下汇编的命令

 

在Ubuntu下汇编的命令是gcc -c hello.s -o hello.o,如图所示。

 

 

4.3 可重定位目标elf格式

在Ubuntu终端中使用代码readelf -a hello.o > ./hello_o_elf.txt 保存hello.o的elf格式。

 

 

ELF文件由4部分组成,分别是ELF头、程序头表、节和节头表。

 

 

ELF头如图所示。

 

它以一个16字节的序列Magic开始,Magic序列描述了生成该文件系统下的字的大小等信息。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移以及节头部表中条目的大小和数量。

 

 

节头如图所示。

 

它包含了文件中出现的各个节的语义,包括节的类型、位置和大小等信息。

 

 

本程序没有程序头。

 

 

重定位节如图所示。

 

 

 

重定位节包含需要进行重定位的信息,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。

 

4.4 Hello.o的结果解析

在Ubuntu终端键入objdump -d -r hello.o >./hello_o.即可得到hello.o的反汇编文件hello_o.s,如图所示。

 

 

与第三章得到的hello.s对比。

 

 

hello_o.s分支转移使用目标跳转的相对寻址,即下一条指令的地址加偏移量,不同于hello.s使用的段名,如图所示。

 

hello_o.s使用十六进制,而hello.s使用十进制。

 

hello_o.s中,call后面是下一条指令的地址,而hello.s中是函数的具体名称。

 

hello_o.s在指令前增加了其十六进制表示。

 

 

4.5 本章小结

本章介绍了汇编的概念和汇编的作用,分析可重定位目标elf格式,重点介绍了其中节头部表、重定位节等信息,还将生成的反汇编文件hello_o.s与hello.s进行对比,比较两者之间的差异。

第5章 链接

5.1 链接的概念与作用

以下格式自行编排,编辑时删除

链接的概念:链接是指从 hello.o 到hello生成过程,即将各种代码和数据片段收集并组合成为一个单一文件的过程,这个文件可被加载到内存并执行。

链接的作用:链接可以使得分离编译成为可能。不需要将一个大型的应用程序组织为一个巨大的源文件,而是可以将它分解成更小、更好管理的模块,可以独立地修改和编译这些模块,当我们改变这些模块中的一个时,只需简单地重新编译它,并重新链接应用,而不必重新编译其他文件。

 

5.2 在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,即可链接得到hello文件,命令如图所示。

 

 

5.3 可执行目标文件hello的格式

在Ubuntu终端键入命令readelf -a hello > ./hello_elf.txt,得到并保存hello的elf文件。

 

 

ELF头中有类别、数据、类型、版本等信息,如图所示。

 

 

 

节头对hello中的信息进行了声明,其中包括了大小、偏移量、起始地址以及数据对齐方式等信息,如图所示。

 

 

程序头包含了虚拟地址内存段等信息,如图所示。

 

重定位节

 

 

 

 

 

 

 

5.4 hello的虚拟地址空间

使用edb加载hello,如图所示。

 

 

可以看出,这一段程序的起始地址是0x401000,由5.3中的elf文件可知,.interp的偏移为000002e0 ,对应edb的0x4012e0位置,同理,.text偏移为000010f0,对应edb的0x4010f0,等等。

 

 

 

 

 

 

5.5 链接的重定位过程分析

在Ubuntu终端键入命令objdump -d -r hello >./hello_.s,生成并保存hello的反编译文件,如图所示。

 

hello_.s中调用相应函数时使用的是已经进行重定位的虚拟地址,区别于未经过链接的hello_o.s中的只有指令,如图所示。

 

 

hello_.s文件里新增了hello.c的代码中使用的库函数,如exit,printf,sleep,atoi,getchar等函数,程序各个节变得更加完整。而未链接的hello_o.s里没有,如图所示。

 

 

 

重定位的过程分为两步。第一步是重定位节和符号定义。连接器将所有相同类型的节合并成为同一类型的新的聚合节。第二步是重定位节中的符号引用。连接器修改代码节和数据节中对每个符号的引用,使得他们指向正确的运行时地址。

5.6 hello的执行流程

 

调用与跳转的各个子程序名或程序地址如图所示。

 

5.7 Hello的动态链接分析

 

查找elf文件可知.got的位置是0000000000403ff0,如图所示。

 

在edb中查看对应位置,值为0,如图所示。

 

dl_inti后再次查看,值发生了变化,如图所示。

 

 

 

 

 

5.8 本章小结

本章介绍了链接的概念和链接的作用。以hello程序为例在Ubuntu中展示链接的过程,以及程序代码在连接前后的一些变化,分析了hello程序的执行流程,以及动态链接。

 

 

 

第6章 hello进程管理

6.1 进程的概念与作用

进程的概念:一个执行中程序的实例。

进程的作用:提供给程序两个关键的抽象。分别是独立的逻辑控制流和私有的地址空间。

 

 

6.2 简述壳Shell-bash的作用与处理流程

壳Shell-bash的作用:Shell是指为使用者提供操作界面的软件。它接受用户命令,然后调用相应的应用程序。Linux系统中所有的可执行文件都可以作为Shell命令来执行。

处理流程:当用户提交了一个命令后,Shell首先判断它是否为内置命令,如果是就通过Shell内部的解释器将其解释为系统功能调用并转交给内核执行;如果是外部命令或实用程序就试图在硬盘中查找该命令并将其调入内存,再将其解释为系统功能调用并转交给内核执行。

 

 

 

 

6.3 Hello的fork进程创建过程

 

父进程通过调用fork函数创建一个新的运行的几乎但不完全和和父进程相同的子进程。子进程得到与父进程用户级虚拟地址空间相同的一份副本,包括代码和数据段、堆、共享库以及用户栈。它们之间的区别是他们有不同的PID。在父进程中,fork返回子进程的PID。在子进程中,fork返回0。

 

 

 

 

6.4 Hello的execve过程

execve函数会在当前进程的上下文中加载并运行一个程序,除非调用失败,execve才会返回到调用程序,正常调用时并不会返回。

 

 

6.5 Hello的进程执行

 

上下文信息

内核重新启动一个被抢占的进程所需要恢复的原来的状态叫做上下文,由寄存器、用户栈、程序计数器、内核栈和内核数据结构等对象的值构成。

 

进程时间片

一个程序被调运行开始到被另一个进程打断之间的时间叫做时间片。

 

进程调度的过程

加载保存的寄存器,切换虚拟地址空间。

 

用户态与核心态转换

用户模式下,进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据。而内核模式进程可以执行指令集中的任何命令,允许访问系统中的内存位置。简而言之,内核模式拥有更高权限。进程只有故障、中断或陷入系统调用时才会得到内核访问权限,其他情况下则处于用户权限之中,保证了系统的安全性。

6.6 hello的异常与信号处理

正常运行

结果如图所示。

 

 


Ctrl-Z如图所示,程序会挂起,但仍在后台运行。

 

 

ps如图所示

 

jobs如图所示

 

pstree如图所示

 

fg如图所示,将程序调回前台。

 

 

 

kill如图所示,会杀死程序。

 

 

 

Ctrl-C如图所示,直接停止程序

 

其他,包括回车,如图所示,并没有影响程序运行。

 

 

 

 

6.7本章小结

本章介绍进程的概念和进程的作用,简述壳Shell-bash的作用与处理流程,说明了Hello的fork进程创建过程,execve过程,并结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等介绍了hello的进程执行。最后,以hello为例,在Ubuntu中演示了对于异常和信号的处理。

 

 

第7章 hello的存储管理

7.1 hello的存储器地址空间

逻辑地址

由程序产生的与段相关的偏移地址部分。逻辑地址指的是机器语言指令中,用来指定一个操作数或者是一条指令的地址。

 

线性地址

逻辑地址到物理地址变换之间的中间层。程序hello的代码会产生段中的偏移地址,加上相应段的基地址就生成了一个线性地址。

 

虚拟地址

由于Linux的一些特性,在Linux下逻辑地址几乎就是虚拟地址,虚拟地址是与物理地址相对的,抽象的地址。

 

物理地址

地址总线上,以电子形式存在的,使得数据总线可以访问主存的某个特定存储单元的内存地址,区别于虚拟地址。

 

7.2 Intel逻辑地址到线性地址的变换-段式管理

段页式管理就是将程序分为多个逻辑段,在每个段里面又进行分页,即将分段和分页组合起来使用。这样做的目的就是想同时获得分段和分页的好处,但又避免了单独分段或单独分页的缺陷。如果将每个段看做一个单独的程序,则逻辑分段就相当于同时加载多个程序。

 

看段选择符的T1=0还是1,知道当前要转换是GDT中的段,还是LDT中的段,再根据相应寄存器,得到其地址和大小。可以查找到对应的段描述符,这样,基地址就知道了。最后,Base + offset,就是要转换的线性地址。

 

 

 

7.3 Hello的线性地址到物理地址的变换-页式管理

页式管理是一种内存空间存储管理的技术,页式管理分为静态页式管理和动态页式管理。将各进程的虚拟空间划分成若干个长度相等的页,页式管理把内存空间按页的大小划分成片或者页面,然后把页式虚拟地址与内存地址建立一一对应页表,并用相应的硬件地址变换机构,来解决离散地址变换问题。页式管理采用请求调页或预调页技术实现了内外存存储器的统一管理。

 

 

 

 

 

 

7.4 TLB与四级页表支持下的VA到PA的变换

转译后备缓冲器,也被翻译为页表缓存、转址旁路缓存,为CPU的一种缓存,由存储器管理单元用于改进虚拟地址到物理地址的转译速度。

处理器生成一个虚拟地址,并将其传送给MMU,MMU向TLB请求对应的PTE,如果命中,则找到对应的物理地址。MMU生成PTE地址,并从高速缓存或者主存请求得到PTE。如果请求不成功,MMU向主存请求PTE,高速缓存或者主存向MMU返回PTE。PTE的有效位为零, 因此 MMU触发缺页异常,缺页处理程序确定物理内存中的牺牲页。缺页处理程序调入新的页面,并更新内存中的PTE。缺页处理程序返回到原来进程,再次执行导致缺页的指令,通过在多级页表中的循环,最终确定物理地址。

 

 

 

 

7.5 三级Cache支持下的物理内存访问

 

如前文所述,找到了物理地址。根据cache大小组数的要求,将PA分为CT(标记位),CI(组索引),CO(块偏移)。根据CI寻找到正确的组,依次与每一行的数据比较,有效位有效且标记位一致则命中。如果命中,则返回想要的数据。如果不命中,就依次去L2,L3,主存判断是否命中,命中时将数据传给CPU同时更新各级cache的储存。

 

 

 

 

7.6 hello进程fork时的内存映射

当fork函数被shell进程调用时,内核为新进程创建各种数据结构,并分配给它的PID,创建当前进程的mm_struct、区域结构和页表的原样副本。这两个进程的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。

 

 

7.7 hello进程execve时的内存映射

execve函数会在当前进程的上下文中加载并运行一个程序,除非调用失败,execve才会返回到调用程序,正常调用时并不会返回。正常运行时,execve会删除已存在的用户区域,映射私有区,映射共享区,最后设置PC。

 

 

 

7.8 缺页故障与缺页中断处理

 

缺页中断指的是当软件试图访问已映射在虚拟地址空间中,但是目前并未被加载在物理内存中的一个分页时,由中央处理器的内存管理单元所发出的中断。分为软性缺页和硬性缺页。

软性页缺失指页缺失发生时,相关的页已经被加载进内存,但是没有向MMU注册的情况。操作系统只需要在MMU中注册相关页对应的物理地址即可。

与软性页缺失相反,硬性页缺失是指相关的页在页缺失发生时未被加载进内存的情况。这时操作系统需要寻找到一个空闲的页。或者把另外一个使用中的页写到磁盘上,并注销在MMU内的记录。然后将数据读入被选定的页,最后向MMU注册该页

 

 

 

7.9动态存储分配管理

动态存储分配,即指在目标程序或操作系统运行阶段动态地为源程序中的量分配存储空间,动态存储分配包括栈式或堆两种分配方式。

动态储存分配管理使用动态内存分配器来进行,例如,在C语言中,printf函数会调用malloc,malloc分配成功返回指向该内存的地址,失败则返回 NULL。

动态内存管理的策略主要有以下两种:带边界标签的隐式空闲链表分配器管理和显式空间链表管理。隐式空闲链表分配器管理中,分配器会搜索空闲链表,查找一个足够大的可以放置所请求块的空闲块。而显式空间链表管理则要求应用显式地释放任何已分配的块。

 

 

 

 

7.10本章小结

本章从hello的存储器地址空间,Intel逻辑地址到线性地址的变换-段式管理,Hello的线性地址到物理地址的变换-页式管理,TLB与四级页表支持下的VA到PA的变换,三级Cache支持下的物理内存访问,hello进程fork时的内存映射,hello进程execve时的内存映射,缺页故障与缺页中断处理,动态存储分配管理等方面介绍了hello的存储管理。

 

第8章 hello的IO管理

8.1 Linux的IO设备管理方法

Linux将文件所有的I/O设备都模型化为文件,设备管理则建立在unix io接口的基础之上。

 

8.2 简述Unix IO接口及其函数

 

打开文件

返回一个小的非负整数,即描述符。用描述符来标识文件。

 

读写文件

读操作:从文件拷贝n个字节到存储器,从当前文件位置k开始,将k增加到k+n,对于一个大小为m字节的文件,当k>=m时,读操作触发一个EOF的条件。

写操作:从存储器拷贝n个字节到文件,k更新为k+n

 

关闭文件

内核释放文件打开时创建的数据结构,并恢复描述符到描述符池中,进程通过调用close函数关闭一个打开的文件。关闭一个已关闭的描述符会出错。

 

改变当前文件位置 

从文件开头起始的字节偏移量。系统内核保持一个文件位置k,对于每个打开的文件,起始值为0。应用程序执行seek,设置当前位置k,通过调用lseek函数,显示地修改当前文件位置。

 

 

 

 

8.3 printf的实现分析

 

 

printf的函数体如图所示

 

在Linux下,write通过执行syscall指令实现了对系统服务的调用,从而使内核执行打印操作。从vsprintf生成显示信息,到write系统函数,到陷阱-系统调用 int 0x80或syscall等。字符显示驱动子程序:从ASCII到字模库到显示vram(存储每一个点的RGB颜色信息)。显示芯片按照刷新频率逐行读取vram,并通过信号线向液晶显示器传输每一个点(RGB分量)。

8.4 getchar的实现分析

getchar函数内容如图所示

 

 

当程序调用getchar时,程序等待用户按键,用户输入的字符被存放在键盘缓冲区中,getchar函数的返回值是用户输入的第一个字符的ascii码,如出错返回-1,且将用户输入的字符回显到屏幕。如用户在按回车之前输入了不止一个字符,其他字符会保留在键盘缓存区中,等待后续getchar调用读取。getchar调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。

 

异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。

8.5本章小结

本章介绍了Linux的I/O设备的基本概念和管理方法,简述了Unix I/O 接口及其函数。最后对printf 函数和 getchar 函数的实现进行了分析。

 

 

 

 

结论

 

hello.c经预处理得到hello.i ,hello.i经编译得到汇编语言文件hello.s,hello.s汇编得到可重定位目标文件hello.o,hello.o经链接得到可执行文件hello。之后通过shell调用fork创建进程,生成子进程,通过execve函数加载运行当前进程的上下文中加载并运行新程序hello,其中涉及寻找地址与读写内存,最终,被父进程回收。

 

一个hello文件的历程就已经很复杂,何况如今的各个大上不少的程序。可见,计算机系统的复杂远不止于此,对计算机系统的学习与体悟也不会止步于此。

 

附件

 

名称

作用

hello.c

程序源代码

hello.i

hello.c经过预处理器cpp生成的文件

hello.s

hello.i经过过编译器ccl生成的文件

hello.o

hello.s经过汇编器as生成可重定位目标程序

hello

hello.o经过链接器ld链接生成可执行文件hello

hello.out

hello反汇编后的可重定位文件

hello_o_elf.txt

hello.o的elf文件

hello_o.s

hello.o的反汇编文件

hello_elf.txt

hello的elf文件

hello_.s

hello的反汇编文件

 

 

参考文献

[1]  林来兴. 空间控制技术[M]. 北京:中国宇航出版社,1992:25-42.

[2]  辛希孟. 信息技术与信息服务国际研讨会论文集:A集[C]. 北京:中国科学出版社,1999.

[3]  赵耀东. 新时代的工业工程师[M/OL]. 台北:天下文化出版社,1998 [1998-09-26]. http://www.ie.nthu.edu.tw/info/ie.newie.htm(Big5).

[4]  谌颖. 空间交会控制理论与方法研究[D]. 哈尔滨:哈尔滨工业大学,1992:8-13.

[5]  KANAMORI H. Shaking Without Quaking[J]. Science,1998,279(5359):2063-2064.

[6]  CHRISTINE M. Plant Physiology: Plant Biology in the Genome Era[J/OL]. Science,1998,281:331-332[1998-09-23]. http://www.sciencemag.org/cgi/ collection/anatmorp.

[7]  Randal E. Bryant, David R. O’Hallaron 深入理解计算机系统.

[8]  [转]printf 函数实现的深入剖析 - Pianistx - 博客园

 

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值