计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 7203610714
班 级 2036014
学 生 胡祖昂
指 导 教 师 刘宏伟
计算机科学与技术学院
2021年5月
本文在Ubuntu虚拟机的环境下,以hello.c为例子对程序的整个生命周期进行了阐释和分析,包含预处理,汇编,编译,链接,以及执行过程中的进程管理和内存管理。
关键词:预处理;汇编;编译;链接;进程;存储
(摘要0分,缺失-1分,根据内容精彩称都酌情加分0-1分)
目 录
第1章 概述
1.1 Hello简介
p2p指program to process,从程序到处理的过程,编辑好的hello.c经过预处理、编译、汇编、链接得到一个可执行程序,然后在bash里通过fork创建进程,然后通过execve执行。
020指内存空间中一开始并没有程序,shell执行时为其分配内存空间,运行结束后回收掉这个进程并删除痕迹,来去无踪。
1.2 环境与工具
X64 CPU;2GHz;2G RAM;256GHD Disk 以上
Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64位/优麒麟 64位 以上;
Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc,objdump
1.3 中间结果
hello.i: 预处理得到的文件。
hello.s: 编译得到的文件。
hello.o:汇编得到的可重定位目标执行文件。
hello:链接得到的可执行目标文件。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
Hello-ld.txt:hello的反汇编文件,用于分析可执行目标文件hello。
Hello-elf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
Hello-ld-elf.txt:hello的ELF格式,用于分析可执行目标文件hello。
1.4 本章小结
第一章简述了p2p,020的过程以及实验的环境和中间结果。
第2章 预处理
2.1 预处理的概念与作用
预处理指进行编译之前做的工作,其指令以“#”开头,包含宏替换,去注释,头文件展开,和条件编译。
作用:代替人来进行一些机械性的文本替换,便于编程
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
图1 Ubuntu中预处理
2.3 Hello的预处理结果解析
图2 hello.i内容
生成了一个非常庞大的.i文件,注释被去除,头文件被展开,函数主体没有变化。
2.4 本章小结
介绍了预处理的内容与作用,对hello.c的预处理过程进行了分析。
第3章 编译
3.1 编译的概念与作用
概念:编译器将.i文件翻译成汇编语言文件.s的过程
作用:将高级的程序语言翻译为便于机器执行的低级指令。编译过程中还能进行语法检查和代码优化,报出不符合规则的错误,并在空间和时间资源消耗上进行优化。
3.2 在Ubuntu下编译的命令
gcc -S hello.i -o hello.s
图3 Ubuntu中编译
3.3 Hello的编译结果解析
图4 编译结果
3.3.1数据类型
- 常量:字符串常量:"用法: Hello 学号 姓名 秒数!\n" 被标识为LC0
- 变量:局部变量i用于控制循环,被翻译成了L2与L3的语句用于控制循环跳转,L2初始化,L3判断循环结束条件,在L4的最后一条指令处进行自增操作
3.3.2赋值
i的赋值语句为L2第一句,给栈顶元素赋值0。
3.3.3算术操作
i的自增操作,在L4最后一句,addl指令把1加给栈顶的i
3.3.4关系操作
1.判断i小于8,被翻译成了比较栈顶的i是否小于等于7,是则回到循环,不是则结束。
2.判断参数个数是否等于4:cmpl $4, -20(%rbp)
3.3.5控制操作
- 通过L2,L3,L4三段指令来回跳转实现了循环,并在L4中放入循环体内的指令
2.cmpl $4, -20(%rbp)
je .L2
进行一个条件跳转来实现if
3.3.6数组
argv[]为数组
movq -32(%rbp), %rax
addq $16, %rax
movq (%rax), %rdx
movq -32(%rbp), %rax
addq $8, %rax
movq (%rax), %rax
movq %rax, %rsi
通过数组首个元素位置加上下标偏移的方式来访问数组元素
3.3.7函数操作
1.Main函数的参数传递
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
将参数放入栈中
2.printf函数
leaq .LC0(%rip), %rdi将字符常量作为参数传入。并通过call puts来调用输出函数
3.sleep和atoi函数
movq %rax, %rdi
call atoi@PLT
movl %eax, %edi
call sleep@PLT
先将参数argv[3]传入atoi并调用函数atoi然后返回值再传给函数sleep,调用sleep。
3.4 本章小结
介绍了编译的概念和作用,在Ubuntu中对hello.c进行了编译,并分析了生成的.s文件,阐述了hello程序的汇编语言如何实现对应c语言中的操作
第4章 汇编
4.1 汇编的概念与作用
概念:指将.s文件翻译为二进制机器语言指令生成.o文件的过程
作用:将汇编程序直接转化为计算机能够执行的二进制机器指令
4.2 在Ubuntu下汇编的命令
gcc -c hello.s -o hello.o
图5 Ubuntu中汇编过程
4.3 可重定位目标elf格式
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
图6 file-header
分析可知文件类型为elf64,数据为补码,采用小端序,类型为REL(可重定位文件),程序头起点为第0个字节,节头起点为第1160个字节,一共有13个节。
图7 section-headers
图8 symbol-table
图9 relocation section(重定位节)
重定位节中包含了rodata(字符串),puts,exit,printf,atoi,sleep和getchar
4.4 Hello.o的结果解析
图10 hello.o反汇编分析
汇编代码左侧即为16进制的机器代码,与同一行的汇编语言相对应。函数调用不再call函数名而是直接跳转到对应的存储位置,分支转移也是直接跳转到对应存储位置而不是标识符
4.5 本章小结
介绍了汇编的概念和作用,并分析了hello.s汇编出的hello.o文件的elf格式和基本信息,对比了其反汇编出的代码和原始汇编代码的区别
第5章 链接
5.1 链接的概念与作用
概念:将多个编译好的目标文件整合成一个可执行文件
作用:使得程序可以模块化,提高编程效率
5.2 在Ubuntu下链接的命令
图11 Ubuntu中链接过程
5.3 可执行目标文件hello的格式
指令:readelf -a hello>hello-ld-elf.txt
图12 hello的elf头
入口地址0x400550,程序头起点为64,节头起点为5936,共有25个节
图13 hello的节头
图14 程序头
5.4 hello的虚拟地址空间
“edb --run hello 7203610714 hza 2”
图15 edb中运行hello
图16 edb中查看虚拟内存
5.5 链接的重定位过程分析
图17 hello的反汇编结果
和hello.o相比main函数之前增加了许多函数,都是重定位的过程中加入进来的,同时地址从4004c0开始,而不再从0开始,可见重定位后的虚拟地址是绝对地址而非相对地址。所有函数的调用指令和跳转指令的地址也都在重定位后发生了变化。
5.6 hello的执行流程
图18 hello在edb中执行
_start
Lib_start_main
Hello!puts@plt
Hello!exit@plt
Hello!printf@plt
Hello!sleep@plt
Hello!atoi@plt
Hello!_init
Hello!.plt
Hello!main
5.7 Hello的动态链接分析
图19 动态链接内容
从ELF中得到got和got.plt的位置,然后去edb中找到0x600ff0和0x601000
5.8 本章小结
介绍了链接的概念作用,在edb中展示并分析了链接后的hello程序发生的变化
第6章 hello进程管理
6.1 进程的概念与作用
作用:进程提供给应用程序两个关键抽象
▪ 逻辑控制流 (Logical control flow)
▪ 每个程序似乎独占地使用CPU
由OS内核通过上下文切换机制实现
▪ 私有地址空间 (Private address space)
▪ 每个程序似乎独占地使用内存系统
由OS内核的虚拟内存机制实现
6.2 简述壳Shell-bash的作用与处理流程
作用:1、shell可以合并编程语言以控制进程和文件,以及启动和控制其他程序2、shell能够减少大量的重复输入和交互操作,能够进行批量的处理和自动化完成维护,减轻管理层的负担
处理流程:读取用户输入的命令,如果是内置命令就执行对应的指令,如果是可执行文件的名字就创建子进程用来执行这个文件。同时可以对键盘输入的控制信号做出实时反应,并在结束之后回收僵尸进程。
6.3 Hello的fork进程创建过程
读取到hello,由于不是内置命令,因此视为可执行程序
Shell创建一个子进程用于运行hello,子进程中fork返回0,父进程中fork返回hello的pid
6.4 Hello的execve过程
int execve(char *filename, char *argv[], char *envp[])
¢ 在当前进程中载入并运行程序
▪ filename:可执行文件
▪ 目标文件或脚本(用#!指明解释器,如 #!/bin/bash)
▪ argv :参数列表,惯例:argv[0]==filename
▪ envp:环境变量列表
▪ "name=value" strings (e.g., USER=droh) ▪ getenv, putenv, printenv
¢ 覆盖当前进程的代码、数据、栈
▪ 保留:有相同的PID,继承已打开的文件描述符和信号上下文
¢ Called once and never returns(调用一次并从不返回)
子进程中调用了execve来载入运行hello
6.5 Hello的进程执行
结合进程上下文信息、进程时间片,阐述进程调度的过程,用户态与核心态转换等等。
上下文信息:核心重新启动进程时需要获取的信息
进程调度过程:调度器控制内核来决定何时中断进程,运行一个新进程。并能通过上下文信息来重新启动曾经中断的进程
用户态与核心态:用户态就是执行程序代码时候的状态,核心态就是因为中断或故障或系统调用等手段将控制交给内核时的状态
例如在hello中,执行到sleep函数时,从用户态切换到核心态,等到计时结束之后再重新切换回用户态继续执行程序
6.6 hello的异常与信号处理
./hello 7203610714 hza 3
先是正常执行,然后中途乱按会被显示在终端里但是并不影响程序的执行
Ctrl+c后程序停止,进程也被回收
Ctrl+z后程序暂停并进入后台,jobs可以把这个作业显示出来,ps也能看到这个进程
Fg命令将其转为前台程序继续执行
Kill则可以将其终止
6.7本章小结
介绍了进程相关的概念,并以hello为例阐述了执行hello的整个流程
第7章 hello的存储管理
7.1 hello的存储器地址空间
逻辑地址:编译器编译程序时,会为程序生成代码段和数据段,然后将所有代码放到代码段中,将所有数据放到数据段中。最后程序中的每句代码和每条数据都会有自己的逻辑地址。
线性地址:CPU加载程序后,会为这个程序分配内存,所分配内存又分为代码段内存和数据段内存。代码段内存的基址保存在CS中,数据段内存的基址保存在DS中。
虚拟地址:虚拟内存中的地址
物理地址:真实物理内存中的地址
7.2 Intel逻辑地址到线性地址的变换-段式管理
逻辑地址由段选择符和偏移量组成,线性地址为段首地址与逻辑地址中的偏移量组成。其中,段首地址存放在段描述符中。而段描述符存放在描述符表中,也就是GDT(全局描述符表)或LDT(局部描述符表)中。
7.3 Hello的线性地址到物理地址的变换-页式管理
线性地址(虚拟地址)由虚拟页号VPN和虚拟页偏移VPO组成。首先,MMU从线性地址中抽取出VPN,并且检查TLB,看他是否因为前面某个内存引用缓存了PTE的一个副本。TLB从VPN中抽取出TLB索引和TLB标记,查找对应组中是否有匹配的条目。若命中,将缓存的PPN返回给MMU。若不命中,MMU需从页表中的PTE中取出PPN,若得到的PTE无效或标记不匹配,就产生缺页,内核需调入所需页面,重新运行加载指令,若有效,则取出PPN。最后将线性地址中的VPO与PPN连接起来就得到了对应的物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换
7.5 三级Cache支持下的物理内存访问
先从L1中取出并寻找,若不命中则到低一级的cache中取出再寻找
7.6 hello进程fork时的内存映射
执行fork()函数时,内核会为新进程分配pid,创建新进程的虚拟内存,新进程会拥有当前进程mm_struct,区域结构,页表的副本。但是两进程页面只读,区域写时复制。
当fork()函数在从新的进程中返回时,hello进程现在的虚拟内存刚好和调用fork时存在的虚拟内存相同但相互独立,映射的也是同一个物理内存。当这两个进程中的任一个进行写操作时,写时复制机制就会创建新页面,在新的页面中进行写操作,并且原来的虚拟内存映射到创建的新页面上。综上所述可以知道,每个进程都具有自己的地址空间。
7.7 hello进程execve时的内存映射
- 首先删除已存在的用户区域,也就是将shell与hello都有的区域结构删除。然后映射私有区域,即为新程序的代码、数据、bss和栈区域创建新的区域结构,均为私有的、写时复制的。
- 映射共享区域,将一些动态链接库映射到hello的虚拟地址空间。
- 设置程序计数器,使之指向使之指向代码区域的入口点。
7.8 缺页故障与缺页中断处理
DRAM缓存不命中称为缺页,缺页异常会调用内核中的缺页异常处理程序,该程序会选择一个牺牲页复制回磁盘并从磁盘把缺少的页复制回来,此时就可以正常读取原本缺少的页了
7.9动态存储分配管理
动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合来维护。每个块就是一个连续的虚拟内存片,要么是已分配的,要么是空闲的。已分配的块显式地保留为供应用程序使用。空闲块可用来分配。空闲块保持空闲,直到它显式地被应用所分配。一个已分配的块保持已分配状态,直到它被释放,这种释放要么是应用程序显式执行的,要么是内存分配器自身隐式执行的。
Printf会调用malloc,请简述动态内存管理的基本方法与策略。
7.10本章小结
讲述了计算机运行程序时的内存管理机制并以hello为例做了简单解释
结论
先编写出源程序hello.c
预处理得到hello.i
编译得到hello.s
汇编得到hello.o
链接得到可执行程序hello
在终端中运行hello时shell先通过fork创建子进程
然后子进程调用函数execve载入并运行hello
中途收到ctrl+z信号停止转入后台
Fg指令则重新启动该程序放入前台运行
运行结束后shell回收掉进程,内核将其清除,至此彻底结束
计算机是最巧妙而精密的系统,是从最基础的01创造出的一切美妙事物,正因如此对于计算机的研究是漫长而深奥的,值得我们花费一生的时间去探索学习
附件
hello.i: 预处理得到的文件。
hello.s: 编译得到的文件。
hello.o:汇编得到的可重定位目标执行文件。
hello:链接得到的可执行目标文件。
hello.txt:hello.o的反汇编文件,用于分析可重定位目标文件hello.o。
Hello-ld.txt:hello的反汇编文件,用于分析可执行目标文件hello。
Hello-elf.txt:hello.o的ELF格式,用于分析可重定位目标文件hello.o。
Hello-ld-elf.txt:hello的ELF格式,用于分析可执行目标文件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.