哈工大计算机系统大作业——程序人生-Hello’s P2P
Posted HITerltr劉天瑞
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了哈工大计算机系统大作业——程序人生-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 020
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021112808
班 级 2103103
学 生 陶丽娜
指 导 教 师 刘宏伟
摘 要
本文详细介绍了hello程序的一生,在Linux环境下借助gcc,cpp,cll,as,ld,vim,objdump,edb等工具,分析 hello程序如何从一个文本文件源程序hello.c经过预处理、编译、汇编、链接成为可执行目标文件,还分析了进程管理、异常与信号处理。通过分析hello的一生,复习了本课程的所有内容。
关键词:编译;预处理;汇编;链接;进程;异常与信号;
计算机科学与技术学院
2022年10月
目录
第1章 概述
1.1Hello简介
P2P过程:(From Program to Process)通过编译器的处理,hello.c文件经历预处理、编译、汇编、链接,四个步骤,从源程序文件变为可执行目标文件,然后由shell为其创建一个新的进程并运行它,此时hello就从program变成了process。
020过程:(From 0 to 0)刚开始程序还不在内存空间中,所以是0,shell通过execve和fork加载并执行hello,映射虚拟内存,进入程序入口后程序开始载入物理内存,然后执行第一条指令,CPU为运行的hello分配时间片执行逻辑控制流。当程序运行结束后,shell父进程负责回收hello进程,内核删除相关数据结构,此时又变成了0。
1.2环境与工具
硬件环境:AMD Ryzen 7 5800H; 16G RAM;512G SSD
软件环境:Windows 10 64位;VirtualBox6.1;Ubuntu 20.04 LTS 64;
开发工具: gcc, objdump, vim
1.3中间结果
名称 | 作用 |
---|---|
hello.c | hello程序c语言源文件 |
hello.i | hello.c预处理生成的文本文件 |
hello.s | 由hello.i编译得到的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
elf.txt | readelf生成的hello.o的elf文件 |
hello | hello.o和其他文件链接得到的可执行目标文件 |
hello1.elf | readelf生成的hello的elf文件 |
注反汇编结果只是命令执行后查看过,并未保存文件。
1.4本章小结
本章介绍了hello的P2P和020过程,分析hello一生需要的软硬件环境和开发工具,还罗列出了中间结果文件的名字及其作用。
第2章 预处理
2.1预处理的概念与作用
预处理是指程序在编译一个c文件之前,预处理器cpp根据以字符#开头的命令(头文件、宏等),修改原始的c程序。比如hello.c中的#include <stdio.h>命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中,结果就得到了另一个C程序,通常是以.i作为文件扩展名。
2.2在Ubuntu下预处理的命令
gcc -m64 -no-pie -fno-PIC -E hello.c -o hello.i
2.3Hello的预处理结果解析
如图展示了部分结果,预处理器cpp处理以#开头的语句,因为<stdio.h >,<unistd.h>,<stdlib.h>都是标准文件库,所以它会在linux系统中的环境变量下寻找这三个库,它们在/usr/include下,然后cpp将这三个库直接插入代码中。因为这三个库中还有#define #ifdef等,所以cpp还需要将这些展开,所以hello.i最后没有#define
2.4本章小结
本章讲述了预处理的概念和作用,并且分析了hello.c经过预处理后生成hello.i的过程,对hello.c来说是cpp读取了这三个系统头文件的内容,并把它插入此程序文本中,然后得到了另一个C程序hello.i。
第3章 编译
3.1编译的概念与作用
编译过程是指编译器ccl做一些语法语义分析,如果没有错误,它就会把文本文件hello.i翻译成ASCII汇编语言文件hello.s,它包含一个汇编语言程序,翻译后的汇编语言程序中,每一条语句都以一种文本格式描述一条低级机器语言指令,为不同高级语言的不同编译器提供了通用的输出语言。
3.2在Ubuntu下编译的命令
gcc -m64 -no-pie -fno-PIC -S hello.i -o hello.s
3.3Hello的编译结果解析
3.3.1数据
1.立即数
汇编语言中立即数是以$开头的。hello.c中的整型常量是以数字出现的,对应了hello.s中的立即数,如图红框中的数字就是hello.c中的整型常量。分别是4与argc比较,exit(1)参数为1,给i赋初值1,i与9比较,i加1,输出argv[1],argv[2]时的下标1,2,以及argv[3]中的下标3。
它们与hello.s中的对应关系如图所示:
2.字符串:
字符串有红框中的两个,即”用法: Hello 学号 姓名 秒数!\\n”和格式串”Hello %s %s\\n”它们都作为为源程序中printf函数中直接输出的部分,存放在了.rodata中。
3.int
a.int i是局部变量存储在寄存器或者栈中,hello.c程序中i是int型4字节局部变量,在程序中声明,存放在-4(%rbp)中。
b. int argc是main中的第一个参数,根据参数传递规则,不需要声明,直接调用就可以。如图,将edi的值传给了-20(%rbp)
4.数组
char *argv[]是指向char型变量的指针, 没有单独声明,在函数执行时在命令行进行输入。argc指向已经分配好连续的连续空间,起始地址为argv,它保存在%rsi中,如图,将rsi的值传给了-32(%rbp)。
3.3.2赋值
对局部变量i赋值即i=0;因为i是双字,所以用的是movl
3.3.3类型转换
atoi(argv[3])用到了强制转换,它把字符串转换成整型数。
3.3.4算术操作
1. 加法:ADD S, D (D <- D + S)
2. 减法:SUB S, D (D <- D - S)
3.3.5关系操作
CMP S1, S2
1. 判断argc是否等于4,使用cmp S1,S2根据S2-S1的值设置条件码,等于0则ZF=1,否则ZF=0,为接下来的跳转做准备。
2.判断i是否小于等于8,将条件码设置为i - 8,为下一句跳转指令做准备
3.3.6数组指针操作
printf中用了argv[1]、argv[2],atoi中用了argv[3]对应在汇编中的操作是寻址后把argv[1]传给%rsi,argv[2]传给%rdx完成printf的参数传递,寻址后把argv[3]传给%rdi,将参数传给atoi。
3.3.7控制转移
1.第一处是判断argc是否等于4,若不等于,则继续执行,若等于,则跳转至L2处继续执行。
2.第二处是无条件跳转,以跳到L4,即循环部分代码。
3.第三处是判断是否达到循环终止条件(i<9),hello.s中是比较i和8,若小于等于则跳回L4重复循环,否则顺序执行。
3.3.8函数调用
1.第一处是调用printf(),输出一个字符串常量,参数存在%rdi中。
2.第二处是调用printf()输出字符串常量和两个char指针argv[1],argv[2]指向的字符串,字符串常量作为参数1在%rdi中,两个指针作为参数2、3分别存在%rsi和%rdx中。
3.第三处是调用atoi(),将%rdi设置为argv[3],call调用atoi函数进行字符串到整型数的转换。
4.第四处是调用sleep(),首先是将atoi转换后的值保存到%edi中,然后调用sleep。
5.第五处是调用exit,参数是1,将立即数1保存到%edi,然后调用exit
6.第六处是调用getchar(),没有参数。
3.4本章小结
本章先分析了从hello.i到hello.s的过程,然后分析了hello.s文件的程序代码,包括C语言中的各种数据类型以及各种操作指令。它比原来更加接近于底层,更加接近于机器。
第4章 汇编
4.1汇编的概念与作用
概念:汇编器as将hello.s翻译成机器语言指令,把这些指令打包成一种叫可重定位目标程序的格式,并将结果保存在目标文件hello.o中。其中hello.o是一个二进制文件。
作用:产生机器语言指令,使得机器能够识别。
4.2在Ubuntu下汇编的命令
gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
4.3可重定位目标ELF格式
4.3.1.ELF头
ELF头以一个16字节的序列开始,这个序列描述了生成该文件的系统的字的大小和字节顺序,剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中ELF头的大小,目标文件的类型(如可重定位、可执行或者共享的)、机器类型(如X86-64)、字节头部表的文件偏移,以及节头部表中条目的大小和数量。
4.3.2节头部表
节头部表中描述其他节的位置和大小,还包括包括节的名称、类型、地址、偏移量、对齐等。
4.3.3.rel.text和.rel.data
.rel.text(重定位节) 一个.text节中位置的列表,包含了.text节中需要进行重定位的信息。.rel.data是被模块引用或定义的所有全局变量的重定位信息。链接时,需要重定位函数位置(exit, printf, atoi, sleep, getchar)和.rodata中LC0和LC1两个字符串中的信息。
4.3.4符号表
.symtab一个符号表,它存放在程序中定义和引用的函数和全局变量的信息。Name是字符串中的字节偏移,指向符号的以null结尾的字符串名字,value是据定义目标的节的起始位置偏移,size是目标的大小(以字节为单位)。Type是符号的种类,有函数、数据、文件等,Binding简写为bing,表示符号是本地的还是全局的,符号表还可以包含各个节的条目,以及对应原始源文件的路径名的条目。
4.4Hello.o的解析结果
反汇编结果:
hello.s:
反汇编和hello.s在代码段很相像,但是反汇编左侧增加了汇编语言对应的机器语言指令。机器语言是由0/1所构成的序列,在终端显示为16进制表示的。
1.分支转移:
hello.s分支转移目标位都是使用.L*表示,hello.o反汇编之后,目标位置变成具体的地址。段名称在hello.s只是助记符,在hello.o机器语言中不存在。
2.函数调用:
hello.s中函数调用是call+函数名,在反汇编文件中目标地址变成了当前的PC值,因为都调用外部函数,所以需要在链接阶段重定位。
3.操作数:
hello.s中立即数是十进制的,反汇编文件中都是二进制的,在终端显示的时候转换成了十六进制。
4.5本章小结
本章分析了从hello.s到hello.c的过程,分析了hello.s的ELF文件信息,对比hello.s和hello.o反汇编代码,分析了汇编指令和机器指令区别和相同点。下一步,hello.o就会通过链接,成为可执行文件。
第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.3可执行目标文件目标ELF格式
1.ELF头
hello的 ELF头和hello.o的包含的信息种类相同,但是它还包括程序的入口点,也就是当程序执行时要执行的第一条地址,程序头和节数量也有了增加。
2. 节头表
与之前的相比,因为邻接,所有的节都有了实际地址。它描述各个节的大小、偏移量和其他属性。链接器链接时,将文件的相同段合并成一个段,根据这个段的大小和偏移量重定位符号的地址。
3.重定位节
与之前的重定位节完全不同,这是链接重定位的结果。
4.符号表
链接进行符号解析后,不在main中定义的符号也有了类型(TYPE),符号表不需要加载到内存。
5.程序头
程序头部表描述了可执行文件的连续的片与连续内存段的映射,我们可以看到根据可执行目标文件的内容初始化两个内存段,代码段和数据段。
5.4Hello的虚拟地址空间
使用edb加载hello,Data Dump 窗口可以查看本进程的虚拟地址空间各段信息,程序从0x00400000处开始存放。再看程序头,它告诉链接器运行时需要加载的内容,它还提供动态链接的信息。每一个表项提供各段在虚拟地址空间和物理地址空间的各方面的信息。
其中phdr显示程序头表;interp必须调用的解释器。load表示需要从二进制文件映射到虚拟地址空间的段,保存常量数据、程序的目标代码等。dynamic 保存动态链接器使用的信息;note辅助信息。gnu_stack是权限标志,标志栈是否可执行。gnu_relro:重定位后内存中只读区域的位置。
5.5链接的重定位过程分析
命令:objdump -d -r hello
分析hello与hello.o的不同:
hello是符号解析和重定位后的结果,链接器会修改hello中数据节和代码节中对每一个符号的引用,使得他们指向正确的运行地址。
1.新的函数:
hello.o与其他库链接,hello.c中使用的库中的函数就被加载进来了,如exit、printf、sleep、getchar、atoi等函数。
2.条件控制和函数调用地址都有改变
3.hello中多了.init和.plt节。
链接的过程:链接就是链接器将各个目标文件组装在一起,文件中的各个函数段按照一定规则累积在一起。
hello重定位:有重定位PC相对引用和绝对引用,对于PC相对引用,将地址改为PC值-跳转目标位置地址。绝对引用则将地址改成该符号的第一个字节所在的地址。
5.6Hello的执行流程
通过edb的调试,逐步记录call命令调用的函数。
地址 | 名称 |
---|---|
0x401000 | <_init> |
0x401020 | <.plt > |
0x401090 | puts@plt |
0x4010a0 | printf@plt |
0x4010b0 | getchar@plt |
0x4010c0 | atoi@plt |
0x4010d0 | exit@plt |
0x4010e0 | sleep@plt |
0x4010f0 | <_start> |
0x401120 | <_dl_relocate_static_pie> |
0x401125 | |
0x4011c0 | <__libc_csu_init> |
0x401230 | <__libc_csu_fini> |
0x401238 | <_fini> |
5.7Hello的动态链接分析
从hello的elf文件可知,.got表的地址为0x0000000000403ff0,在edb中的Data Dump窗口跳转,定位到GOT表处。
调用_init后
初始地址0x00600ff0全为0。程序调用一个由共享库定义的函数,编译器没有办法预测这个函数的运行时地址,因为它定义它的共享模块在运行时可以加载到任意位置。链接器采用延迟绑定的策略解决。动态链接器使用过程链接表PLT + 全局变量偏移表GOT实现函数的动态链接,GOT中存放目标函数的地址,PLT使用该地址跳转到目标位置,其中GOT[1]指向重定位表,GOT[2]指向动态链接器ld-linux.so运行地址。
5.8本章小结
本章介绍了链接的定义和作用,分析了程序链接的过程,查看ELF的信息分析链接生成可执行文件过程中程序发生的变化。使用edb分析了动态链接。
第6章 HELLO进程管理
6.1进程的概念与作用
概念:进程时一个执行中程序的实例。
作用:系统中的每个程序都运行在某个进程的上下文中,进程提供一个假象,就好像我们的程序是系统中当前运行的唯一的程序的一样,我们的程序好像是独占地使用处理器和内存。处理器就好像是无间断地一条接着一条地执行我们程序中的指令。最后,我们程序中的代码和数据好像是系统内存中唯一的对象。
6.2简述壳SHELL-BASH的作用与处理流程
作用:Shell能解释用户输入的命令,将它传递给内核,还可以:调用其他程序,给其他程序传递数据或参数,并获取程序的处理结果。在多个程序之间传递数据,把一个程序的输出作为另一个程序的输入。Shell本身也可以被其他程序调用。
处理流程:
- 输出一个提示符,等待输入命令,读取从终端输入的用户命令。
- 分析命令行参数,构造传递给execve的argv向量。
- 检查第一个命令行参数是否是一个内置的shell命令,如果是立即执行。
- 否则用fork为其分配子进程并运行。
- 子进程中,进行步骤2获得参数,调用exceve()执行程序。
- 命令行末尾没有&,代表前台作业,shell用waitpid等待作业终止后返回。
- 命令行末尾有&,代表后台作业,shell返回。
6.3HELLO的FORK进程创建过程
输入命令执行hello后,父进程判断不是内部指令后,会通过fork创建子进程。子进程几乎但不完全与父进程相同,子进程得到与父进程用户级虚拟空间相同但独立的副本,包括代码和数据段、堆、共享库以及用户栈。子进程可以读写父进程中打开的任何文件,二者最大的区别在于它们有不同的PID。
fork函数只会被调用一次,但会返回两次,在父进程中,fork返回子进程的PID,在子进程中,fork返回0。
6.4HELLO的EXECVE过程
execve函数在加载并运行可执行目标文件hello,且带列表argv和环境变量列表envp。当出现错误时,例如找不到hello时,execve会返回到调用程序,与一次调用两次返回的fork不同。在execve加载了hello之后,它调用启动代码。启动代码设置栈,并将控制传递给新程序的主函数,该主函数有如下的原型:int main(intargc , char **argv , char *envp);结合虚拟空间和内存映射过程,有删除已存在的用户区域,映射私有区,映射共享区,设置PC这四个过程,进程地址空间如图所示。
6.5HELLO的进程执行
1.逻辑控制流:一系列程序计数器 PC 的值的序列叫做逻辑控制流。这个序列每个PC值唯一对应于包含在程序的可执行目标文件中的指令,或是包含在运行时动态链接到程序的共享对象中的指令,一个进程有一个逻辑控制流,进程交错执行。
2.上下文:内核重新启动一个被抢占的进程所需要恢复的原来的状态,由寄存器、程序计数器、用户栈、内核栈和内核数据结构等对象的值构成。
3.用户模式和内核模式:处理器使用某个控制寄存器一个控制位提供两种模式的区分。用户模式的进程不允许执行特殊指令,不允许直接引用地址空间中内核区的代码和数据;内核模式进程可以执行指令集中的任何命令,并且可以访问系统中的任何内存位置。
4.进程时间片:一个进程和执行它的控制流的一部分的每一时间段。
5.上下文切换:比如sleep函数,初始时,控制在hello中,处于用户模式,调用系统函数sleep后,转到内核模式,调用进程被挂起,经过设定秒数后,发送中断信号,转回用户模式,继续执行指令。如图是一个进程上下文切换的剖析。
调度过程:
在进程执行的某些时刻,内核可以决定抢占当前进程,并重新开始一个先前被抢占了的进程,这种决策就叫做调度,是由内核中称为调度器的代码处理的。当内核选择一个新的进程运行,我们说内核调度了这个进程。在内核调度了一个新的进程运行了之后,它就抢占了当前进程,并使用上下文切换机制来将控制转移到新的进程。
6.6HELLO的异常与信号处理
1.异常,如图所示,异常有以下四种,分别是中断、陷阱、故障、终止。它的种类和处理方式如图所示。
2.信号,信号有很多种,图中展示的是Linux上支持的30种不同类型的信号。
3.hello正常运行状态
4.Ctrl+Z
进程收到 SIGSTP 信号, hello 进程被挂起。ps查看它的进程PID,可知 hello的PID是3333; jobs查看hello的后台 job号是1,用调用 fg 1把它调回前台。
5. Ctrl+C:进程收到 SIGINT 信号,终止 hello。在ps中没有它的PID,在job中也没有,可以看出hello已经被永远地停止了。
6.运行中不停乱按,会将屏幕的输入缓存到缓冲区,乱码被认为是命令。
7.kill,杀死进程,hello被终止。
6.7本章小结
本章介绍了hello进程的执行过程。主要是hello的创建、加载和终止,通过键盘输入。在hello运行过程中,内核有选择地对其进行管理,决定何时进行上下文切换。在hello运行过程中,接受到不同的异常信号时,异常处理程序将对异常信号做出回应,执行对应指令,每种信号有不同的处理机制,对不同的异常信号,hello有不同的处理结果。
结论
hello经历的过程
1.源文件编写:用文本编辑器写出hello的源程序文件。
2.预处理:预处理器对hello.c进行预处理,生成hello.i文本文件,将源程序中使用到的外部库插入到文件中。
3.编译:编译器对hello.i进行语法分析、优化等操作生成hello.s汇编文件。
4.汇编:as将hello.s翻译机器更易读懂的机器代码hello.o,它是一个二进制文件。
5.链接:链接器ld将hello.o和其他用到的文件进行合并链接,生成可执行文件hello。
6.运行程序:在终端中输入运行命令,shell进程调用fork为hello创建子程序,然后调用execve启动加载器,加映射虚拟内存。
7.执行指令:CPU为程序分配时间片,在一个时间片中hello使用CPU资源顺序执行控制逻辑流。
8.异常处理:hello执行的过程中可能收到来自键盘输入的信号,调用信号处理程序进行处理。
9. 进程结束:shell作为父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感悟:通过分析hello的一生,把本课程的知识联系到了一起,不再是以往零碎的知识点了,有了更加完整的知识体系。
附件
名称 | 作用 |
---|---|
hello.c | hello程序c语言源文件 |
hello.i | hello.c预处理生成的文本文件 |
hello.s | 由hello.i编译得到的汇编文件 |
hello.o | hello.s汇编得到的可重定位目标文件 |
elf.txt | readelf生成的hello.o的elf文件 |
hello | hello.o和其他文件链接得到的可执行目标文件 |
hello1.elf | readelf生成的hello的elf文件 |
参考文献
[1] 龚奕利. 深入理解计算机系统.北京:机械工业出版社,2016.
以上是关于哈工大计算机系统大作业——程序人生-Hello’s P2P的主要内容,如果未能解决你的问题,请参考以下文章