哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process
Posted Birdy66
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈工大 计算机系统大作业 程序人生-Hello’s P2P From Program to Process相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算学部
学 号 120L020512
班 级 2003004
学 生 黄鹏程
指 导 教 师 史先俊
计算机科学与技术学院
2022年5月
本次大作业旨在通过对hello程序生命历程中各个环节的实验与分析,将计算机系统课程的整体知识进行串联与复现,从而加深对课程内容的理解。
关键词:计算机;汇编;进程;存储管理;
目 录
2.2在Ubuntu下预处理的命令............................................................................. - 6 -
5.3 可执行目标文件hello的格式...................................................................... - 19 -
6.2 简述壳Shell-bash的作用与处理流程........................................................ - 27 -
6.3 Hello的fork进程创建过程......................................................................... - 28 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................... - 32 -
7.3 Hello的线性地址到物理地址的变换-页式管理......................................... - 33 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................ - 34 -
7.5 三级Cache支持下的物理内存访问............................................................. - 34 -
7.6 hello进程fork时的内存映射..................................................................... - 35 -
7.7 hello进程execve时的内存映射................................................................. - 36 -
7.8 缺页故障与缺页中断处理.............................................................................. - 36 -
8.2 简述Unix IO接口及其函数.......................................................................... - 38 -
第1章 概述
1.1 Hello简介
P2P:
如图1.11,为c语言代码源文件,即hello.c变成可执行文件hello的过程。预处理器对源文件进行宏替换、条件编译的预处理操作后,生成hello.i文件;.i文件检查语法后生成汇编文件hello.s;汇编文件经过汇编被转换为机器码,生成可重定位文件hello.o;然后连接器将源代码中用到的库函数与可重定位文件合并为可执行文件hello;我们在shell中键入命令后,其fork子进程、调用execve加载hello并运行。
图1.11 P2P过程
O2O:
在shell中fork子进程后调用execve加载并执行hello,分配虚拟内存空间并映射到物理内存;随后依照CPU中逻辑控制流开始执行;在程序结束后,shell通过hello父进程或祖先进程将其回收,释放内存空间。
1.2 环境与工具
硬件环境:处理器Intel(R) Core(TM) i5-8300H CPU @ 2.30GHz 2.30 GHz;RAM 8GB; 系统类型:系统类型:64位操作系统,基于x64的处理器;
软件环境:Windows10 64位;Ubuntu 20.04
开发与调试工具:gcc,as,ld,vim,edb,readelf,VS
1.3 中间结果
hello.i:预处理得到的文本文件
hello.s:编译得到的汇编文件
hello.o:汇编得到的可重定位目标文件
hello:链接得到的可执行文件
objdump_hello.s:hello反汇编得到的代码
1.4 本章小结
本章在对P2P、O2O的介绍中概括了hello从诞生到执行再到死亡的过程;给出本次作业的实验环境与用到的中间结果。
第2章 预处理
2.1 预处理的概念与作用
概念:
程序设计领域中,预处理一般是指在程序源代码被翻译为目标代码的过程中,生成二进制代码之前的过程。预处理中会展开以#起始的行,试图解释为预处理指令。包括#if/#ifdef/#ifndef/#else/#elif/#endif(条件编译)、#define(宏定义)、#include(源文件包含)、#line(行控制)、#error(错误指令)、#pragma(和实现相关的杂注)以及单独的#(空指令)。预处理指令一般被用来使源代码在不同的执行环境中被方便的修改或者编译。
作用:
将源文件中用#include形式声明的文件复制至新的程序中;
用实际值替换用#define 定义的字符串,即将宏定义进行替换;
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i如下图2.21,生成文件如图2.22;
图2.21 执行命令
图2.22 生成文件
2.3 Hello的预处理结果解析
打开hello.i,发现程序已经扩展为3060行,如图2.31,hello.c中main函数代码出现在3047行以后,在此之前的代码为.c源文件中含有三个库:#include <stdio.h>、#include <unistd.h>、#include <stdlib.h>的展开,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件,如图2.32。
图2.31 main函数
图2.32 预处理#include
2.4 本章小结
本章使用gcc -m64 -no-pie -fno-PIC hello.c -E -o hello.i将hello.c预处理为hello.i,发现hello.i中插入了大量源文件包含的库文件
第3章 编译
3.1 编译的概念与作用
概念:
编译是指编译器做词法分析、语法分析、语义分析等,在检查无错误后,将代码翻译成汇编语言的过程。 编译器将文本文件 hello.i 翻译成文本文件 hello.s。
作用:
- 语法分析:将不符合语法规则的记号识别出其位置并产生错误提示语句;
- 代码优化:指对程序进行多种等价变换,变为功能等价,但占用资源更少的代码;
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
图3.21 编译的命令
图3.22 生成hello.s
3.3 Hello的编译结果解析
3.3.1 数据
- 常量
数字:如图3.11,3.12源文件中的常量4与8被作为立即数保存在图3.13、3.14中,hello.s的代码节.data中;
图 3.11
图 3.12
图 3.13 立即数4
图 3.14 立即数7(借助7来判断小于8)
字符串:如图3.15,源文件中的字符串"用法: Hello 学号 姓名 秒数!\\n"被保存在.rodata节中,因为其为只读字符串;
图 3.15 保存在.rodata的字符串
- 变量
局部变量:源文件中使用局部变量i:
for ( i = 0; i < 8 ; i++ )
被保存在栈中%rsp-4位置,如图3.16,该位置每次加1后与7进行比较,依此决定是否再次进入循环。
图 3.16 寄存器中的局部变量i
3.3.2 赋值
上文中,局部变量i被赋初值为0,而我们已经知道了它的存储位置,故容易找到其在hello.s中的赋值语句,如图3.21
图 3.21 i赋初值为0
3.3.2 算术操作
对于上文提到的循环,步长为1,每次i自增1,易找到其在hello.s中的操作如图3.22
图 3.22 i++
3.3.3 关系操作、控制转移
源文件中出现了两次关系判断,如图3.31中的13行、17行:
图 3.31
第13行在hello.s中对应操作如图3.32,为argc与4进行比较,若相等则进行跳转操作:
图 3.32 判断相等与跳转操作
上图中,24行为argc与4进行比较,比较的结果保存在寄存器中,25行je根据比较结果决定是否跳转到.L2;
图 3.33 循环中i的比较与跳转操作
3.3.4 数组/指针/结构操作
指针数组char *argv[ ] 首地址保存在-32(%rbp)位置,如图3.41,print函数打印argv[1]与argv[2],则在第35、38行分别将数组首地址加上偏移获得数组元素;
图 3.41
3.3.5 函数操作
参数传递:第1~6个参数储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9这六个寄存器中,剩下的参数保存在栈当中。
调用函数:每个函数的每次调用,都有它自己独立的一个栈帧,这个栈帧中维持着所需要的各种信息。寄存器%ebp指向当前的栈帧的底部(高地址),寄存器%esp指向当前的栈帧的顶部(低地址)。调用函数的栈底将会被保存,而栈顶将作为被调用函数的栈底。
函数返回:函数返回值保存在%ax中。
下对hello.s中函数分析:
- main函数
参数传递:传入参数argc和argv[],分别用寄存器%rdi和%rsi存储
函数调用:C程序总是从mian函数开始执行
函数返回:结束时更改%eax为0
图 3.51 main入口
图 3.52 main出口
- print函数
参数传递: call printf时传入了 argv[1]和argc[2]的地址
图 3.53 调用printf时传入的argv[1]和
argc[2]保存在%rdx、%rsi中
- exit()函数
参数传递:将%edi 设置为 1 - atoi()函数
参数传递:将%rdi 设置为 argv[3] - sleep()函数
参数传递:将%edi 设置为atoi处理后的argv[3] - exit()函数
参数传递:将%edi 设置为 1,执行exit(1)
3.4 本章小结
编译是指编译器做词法分析、语法分析、语义分析等,在检查无错误后,将代码翻译成汇编语言的过程。本章对hello.i编译后得到的hello.s进行分析,探究了编译器处理C语言的各个数据类型以及各类操作的过程。
第4章 汇编
4.1 汇编的概念与作用
概念:把汇编语言翻译成机器语言的过程称为汇编。
作用:用汇编语言编写的程序,机器不能直接识别,要由一种程序将汇编语言翻译成机器语言,这种起翻译作用的程序叫汇编程序。
4.2 在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC hello.s -c -o hello.o
图4.21 汇编的命令
图 4.22 汇编得到hello.o
4.3 可重定位目标elf格式
readelf -a hello.o > helloo.elf 生成文本文件
ELF头:
以16B的序列 Magic 开始,Magic描述了生成该文件的系统的字的大小和字节顺序,ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头的大小、目标文件的类型、机器类型、 字节头部表(section header table)的文件偏移,及节头部表中条目的大小和数量等信息。根据头文件的信息,可以知道该文件是可重定位目标文件,有14个节。
图 4.31 ELF头
节头:
描述了.o文件中出现的各个节的类型、位置、所占空间大小等信息。
图 4.32 节头
重定位节:
当汇编器生成一个目标模块时,它并不知道数据和代码最终将放在内存中的什么位置。它也不知道这个模块引用的任何外部定义的函数或者全局变量的位置。所以,无论何时汇编器遇到最终未知未知的目标引用,它就会生成一个重定位条目,告诉链接器在将目标文件合并到可执行文件时如何修改新的引用。
本程序需要重定位的信息有:.rodata中的模式串,puts,exit,printf,slepsecs,sleep,getchar这些符号。
图 4.33 重定位节
符号表:
符号表中存放程序定义和引用的函数和全局变量的信息。但其中不包含局部变量条目。
图 4.34 符号表
分析hello.o的ELF格式,用readelf等列出其各节的基本信息,特别是重定位项目分析。
4.4 Hello.o的结果解析
图 4.41 hello.o反汇编
将其与hello.s进行对照分析:
- 操作数:hello.s中操作数为十进制,而反汇编代码中为十六进制;
- 分支转移:hello.s中地址使用段名称如 je .L2,而反汇编代码中则使用相对偏移地址,如 je 2d <main+0x2d>;
- 函数调用:hello.s中,call指令使用的是函数名称,反汇编代码中call指令使用相对偏移地址。原因是hello.s中调用的函数都是共享库中的函数,故需要通过等待调用动态链接将重定位的函数目标地址链接到共享库程序中,最终需通过动态链接器确定函数的运行时地址;
- 除上述以外,二者没有什么不同,这表明了汇编语言能与机器码建立一一对应的关系。
4.5 本章小结
把汇编语言翻译成机器语言的过程称为汇编,hello.s汇编得到hello.o后,我们阅读了ELF文件,然后通过hello.o反汇编得到的代码与hello.s进行比较,发现机器码与汇编语言的映射关系,以及它相对汇编语言所具有的特点。
第5章 链接
5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个单一文件的过程,这个文件可被加载(复制)到内存并执行。
作用:节省源程序空间;将可重定位文件连接成为可执行文件。
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
图 5.21 链接的命令
图 5.22 生成可执行文件
5.3 可执行目标文件hello的格式
ELF文件头:
图5.31 ELF文件头
节头:
节头部表中包含了hello中所有节的信息,其中包括名称、类型、大小、地址和偏移量等信息,其中地址为程序被载入到虚拟地址的起始地址,偏移量为各个节在程序中的偏移量。根据节头部表的信息可以使用HexEdit定位各个节的起始位置及大小。
图5.32 节头
程序头:
elf可执行文件易加载到内存,可执行文件连续的片被映射到连续的内存段,程序头部表描述了这一映射关系。程序头部表包括各程序头的偏移量、内存地址、对其要求、目标文件与内存中的段大小及运行时访问权限等信息。
图5.33 程序头
重定位节:
重定位节包含.text节中一些需对目标进行重定位的函数信息,链接器把函数的目标位置文件与其他目标文件组合在一起时,需要修改这些函数的位置。
图5.34 重定位节
5.4 hello的虚拟地址空间
在edb的memory regions窗口中,可以看到hello的虚拟地址空间,如图5.41,由0x400000到0x405000;
图5.41 memory regions窗口
edb加载hello后, Data Dump 窗口可以查看加载到虚拟地址中的 hello 程序,如图5.42;
图5.42 Data Dump 窗口
根据5.3中的节头部表中的地址,可在edb中找到各个节。例如:.text节的地址为0x4010f0,大小为0x145,用edb查找结果如图5.43;
图5.43
.data节的地址为0x404040,大小为0x4,如图5.44
图5.44
.rodata节的地址为0x402000,大小为0x3b,如图5.45
图5.45
5.5 链接的重定位过程分析
使用命令:objdump -d -r hello >helloobjdump.txt,获得hello的反汇编代码,如图5.51;
图5.51 hello反汇编
对照hello与hello.o,不同点有:
- 在hello.o中,main函数地址从0开始,hello.o中保存的是相对偏移地址;而在hello中main函数0x401125开始(如图5.52),即hello中保存虚拟内存地址,对hello.o中的地址进行了重定位。
图5.52 hello的main函数
- ELF描述文件总体格式,发现它包括了程序的入口点,即程序运行时执行的第一条指令的地址。由于可执行文件是完全链接的,因此没有rel节。
- hello中多了.init节和.plt段,如图5.53。.init节定义函数_init,用于程序的初始化代码,还有初始化程序执行环境;.plt段为程序执行时的动态链接。所有重定位条目都被修改为确定的运行时内存地址。
图5.53 .init节.plt段
- 在hello中链接加入了在hello.c中用到的函数,如exit、printf、sleep、getchar等函数。
链接过程:
经以上分析可知,链接就是将多个可重定位目标文件合并到一起,生成可执行文件。链接需要进行符号解析、重定位及计算符号引用地址三个步骤。
重定位:
重定位将合并输入模块。并为每个符号分配运行地址。重定位由两个步骤组成:重定位节与符号定义、重定位节中的符号引用。
定位节与符号定义,链接器将相同类型的节合并为同一类型的新的聚合节,此后链接器将运行时内存地址赋值新的聚合节、输入模块定义的每个节,还有输入模块定义的每个符号。
重定位节中的符号引用,链接器修改代码节与数据节中对每个符号的引用,使他们指向正确的运行地址,这一步依赖hello.o中的重定位条目。
5.6 hello的执行流程
Edb逐步执行并记录调用的函数,如图5.61
图5.61 即将调用hello!_init
得到调用函数顺序如下:
ld-2.27.so!_dl_start
ld-2.27.so!_dl_init
hello!_start
libc-2.27.so!__libc_start_main
-libc-2.27.so!__cxa_atexit
-libc-2.27.so!__libc_csu_init
hello!_init
libc-2.27.so!_setjmp
-libc-2.27.so!_sigsetjmp
–libc-2.27.so!__sigjmp_save
hello!main
hello!puts@plt
hello!exit@plt
hello!printf@plt
hello!atoi@plt
hello!sleep@plt
hello!getchar@plt
ld-2.27.so!_dl_runtime_resolve_xsave -ld-2.27.so!_dl_fixup
–ld-2.27.so!_dl_lookup_symbol_x
libc-2.27.so!exit
5.7 Hello的动态链接分析
在elf文件中可以找到动态链接调用的函数的位置,如图5.71:
图5.71 函数位置
动态链接调用的函数的位置为0x404000,进入edb内存窗口查看:
图5.71 init之前
图5.72 init之后
对于变量而言,利用代码段和数据段的相对位置不变的原则计算得到正确地址。对于库函数而言,需要plt与got合作,plt初始存的是一批代码,它们跳转到got所指示位置,接着调用链接器。初始时got里面存的都为plt的第二条指令,随后链接器会修改got,当下一次再次调用plt时,指向的就是正确的内存地址。plt就能跳转到正确的区域。
5.8 本章小结
本章分析了hello的链接过程,hello.o经过链接生成可执行文件hello,通过对比hello反汇编文件与hello.o之间的差别,我们可以总结出重定位的一些特点。在edb中逐步调试hello,我们能看到hello逐个调用的函数。
第6章 hello进程管理
6.1 进程的概念与作用
概念:
进程是计算机中的程序关于某数据集合上的一次运行活动,是系统进行资源分配和调度的基本单位,是操作系统结构的基础。在早期面向进程设计的计算机结构中,进程是程序的基本执行实体;在当代面向线程设计的计算机结构中,进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。
作用:
(1) 给程序创造这样的假象: 我们的程序好像是系统中当前运行的唯一程序一样,我们的程序好像是独占的使用处理器和内存。
(2) 处理器好像是无间断的执行我们程序中的指令,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2 简述壳Shell-bash的作用与处理流程
作用:shell是系统的用户界面,它接收并解释用户输入的命令,再将其送入内核执行。
处理流程:
- 读取从键盘输入的命令;
- 判断命令是否正确,并判断命令是否为内置命令:
若为内置命令则立即执行;
否则将命令行的参数改造为系统调用execve()内部处理所要求的形式终端进程调用fork()来创建子进程,自身则用系统调用wait()来等待子进程完成;
- 当子进程运行时,它调用execve()根据命令的名字指定的文件到目录中查找可行性文件,调入内存并执行这个命令;
- 如果命令行未尾有后台命令符号&终端进程不执行等待系统调用,而是立即发提示符,让用户输入下一条命令;如果命令末尾没有&则终端进程要一直等待;
- 当子进程完成处理后,向父进程报告,此时终端进程被唤醒,做完必要的判别工作后,再发提示符,让用户输入新命令。
6.3 Hello的fork进程创建过程
父进程在读取命令后,首先判断该命令是否为内置命令,若非内置命令,则会调用fork命令创建子进程。子进程除PID外,与父进程完全一致,获得与父进程虚拟地址空间相同但独立的副本,其用户栈、寄存器、代码段等也与父进程一致,子进程可以读写父进程打开的任何文件。Fork函数在父进程中返回子进程PID,在子进程中则返回0。
计算机系统
大作业
题 目 程序人生-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 From Program to Process的主要内容,如果未能解决你的问题,请参考以下文章