HIT-CSAPP程序人生大作业
Posted lala baba
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了HIT-CSAPP程序人生大作业相关的知识,希望对你有一定的参考价值。
计算机系统
大作业
计算机科学与技术学院
2022年11月
摘 要
本文基于《深入理解计算机系统》,通过对一个简单的程序Hello在Linux环境下的生命周期的分析,论述其从hello.c经过预处理、编译、汇编、链接等一系列操作生成可执行文件hello,再通过程序对进程的管理、内存空间的分配、信号和异常的处理、对 I/O 设备的调用等环节彻底解释hello从创建到结束的过程,进而加深对计算机系统的理解。
关键词:计算机系统;程序的生命周期;hello的一生;程序编译过程
目 录
第1章 概述................................................... - 4 -
1.1 Hello简介............................................ - 4 -
1.2 环境与工具........................................... - 4 -
1.3 中间结果............................................... - 4 -
1.4 本章小结............................................... - 4 -
第2章 预处理............................................... - 5 -
2.1 预处理的概念与作用........................... - 5 -
2.2在Ubuntu下预处理的命令................ - 5 -
2.3 Hello的预处理结果解析.................... - 5 -
2.4 本章小结............................................... - 5 -
第3章 编译................................................... - 6 -
3.1 编译的概念与作用............................... - 6 -
3.2 在Ubuntu下编译的命令.................... - 6 -
3.3 Hello的编译结果解析........................ - 6 -
3.4 本章小结............................................... - 6 -
第4章 汇编................................................... - 7 -
4.1 汇编的概念与作用............................... - 7 -
4.2 在Ubuntu下汇编的命令.................... - 7 -
4.3 可重定位目标elf格式........................ - 7 -
4.4 Hello.o的结果解析............................. - 7 -
4.5 本章小结............................................... - 7 -
第5章 链接................................................... - 8 -
5.1 链接的概念与作用............................... - 8 -
5.2 在Ubuntu下链接的命令.................... - 8 -
5.3 可执行目标文件hello的格式........... - 8 -
5.4 hello的虚拟地址空间......................... - 8 -
5.5 链接的重定位过程分析....................... - 8 -
5.6 hello的执行流程................................. - 8 -
5.7 Hello的动态链接分析........................ - 8 -
5.8 本章小结............................................... - 9 -
第6章 hello进程管理.......................... - 10 -
6.1 进程的概念与作用............................. - 10 -
6.2 简述壳Shell-bash的作用与处理流程.. - 10 -
6.3 Hello的fork进程创建过程............ - 10 -
6.4 Hello的execve过程........................ - 10 -
6.5 Hello的进程执行.............................. - 10 -
6.6 hello的异常与信号处理................... - 10 -
6.7本章小结.............................................. - 10 -
第7章 hello的存储管理...................... - 11 -
7.1 hello的存储器地址空间................... - 11 -
7.2 Intel逻辑地址到线性地址的变换-段式管理............................................................ - 11 -
7.3 Hello的线性地址到物理地址的变换-页式管理........................................................ - 11 -
7.4 TLB与四级页表支持下的VA到PA的变换................................................................ - 11 -
7.6 hello进程fork时的内存映射......... - 11 -
7.7 hello进程execve时的内存映射..... - 11 -
7.8 缺页故障与缺页中断处理................. - 11 -
7.9动态存储分配管理.............................. - 11 -
7.10本章小结............................................ - 12 -
第8章 hello的IO管理....................... - 13 -
8.1 Linux的IO设备管理方法................. - 13 -
8.2 简述Unix IO接口及其函数.............. - 13 -
8.3 printf的实现分析.............................. - 13 -
8.4 getchar的实现分析.......................... - 13 -
8.5本章小结.............................................. - 13 -
结论............................................................... - 14 -
附件............................................................... - 15 -
参考文献....................................................... - 16 -
第1章 概述
1.1 Hello简介
1.1.1 P2P: From Program to Process
P2P,即从程序到进程。用户利用高级语言C语言编写hello.c源程序,经过cpp预处理形成hello.i,再经过ccl编译形成汇编语言程序hello.s,然后经过as转换为机器语言指令,形成可重定位目标程序hello.o,最后通过ld与库函数链接并符号解析与重定位,形成可执行目标文件hello。而后可执行文件hello通过shell加载,fork产生子进程,经过以上步骤hello程序(program)变成了hello进程(process)。
1.1.2 020: From Zero-0 to Zero -0
020,即从运行到结束。初始时内存中没有hello文件相关的内容,通过fork产生hello子进程后,通过execve进行加载,先删除当前虚拟地址已存在的数据结构,为hello的代码、数据、bss等创建区域,然后映射共享区域,设置程序计数器,进入main函数,CPU分配时间片执行逻辑控制流。执行过程中,虚拟内存为进程提供独立的空间;存储结构层层递进,让数据从磁盘传输到CPU中;TLB、分级页表等也为数据的高效访问提供保障;I/O设备通过描述符与接口实现了hello的输入输出。多方面合作配合之下,hello完成执行。然后,shell回收hello进程,删除hello的所有痕迹,释放运行中占用的内存空间。至此,hello从运行到结束,完成020过程。
1.2 环境与工具
1.2.1 硬件环境
X64 CPU;2GHz;2G RAM;256GHD Disk
1.2.2 软件环境
Windows 10 64位;Vmware-workstation-16;Ubuntu 20.04 LTS 64位
1.2.3 开发与调试工具
Codeblocks 64位;vi/vim/gedit+gcc
1.3 中间结果
表1.3-1 中间结果
文件名 | 功能 |
hello.c | 源代码 |
hello.i | 预处理后的文本文件 |
hello.s | 编译后的汇编文件 |
hello.o | 汇编后的可重定位目标文件 |
hello | 链接后的可执行文件 |
hello.elf | 用readelf读取hello.o的ELF格式信息 |
hello.asm | 反汇编hello.o的反汇编文件 |
hello2.elf | 由hello可执行文件生成的.elf文件 |
hello2.asm | 反汇编hello可执行文件得到的反汇编文件 |
1.4 本章小结
本章主要介绍了hello的P2P和020过程,从总体上简要阐述了hello的一生,给出了论文研究时的环境与工具以及中间生成的文件信息。
第2章 预处理
2.1 预处理的概念与作用
2.1.1 预处理的概念
预处理步骤是指程序开始运行时,预处理器(cpp)根据以字符#开头的命令,修改原始的C程序的过程。
2.1.2 预处理的作用
- 宏展开:将所有的宏定义进行替换,然后删除#define
- 条件编译:如果源代码中包含条件预处理指令(如#if),就会先判断条件,再修改源代码
- 头文件展开:对文件包含命令#include,引入对应头文件,将头文件的内容(.h)插入到命令所在位置,从而把头文件和当前源文件连接成一个源文。
2.2在Ubuntu下预处理的命令
预处理命令:gcc -E hello.c -o hello.i
图2.2-1 预处理命令截图
2.3 Hello的预处理结果解析
预处理后的源文件从23行扩展到了3060行,main函数在第3047行,预处理器将头文件中的内容引入hello.i,将需要用到的库函数等加入到了文本中,让程序能够继续被编译器编译。
- 包含文件信息
图2.3-1 hello.i中部分文件信息
- 类型定义信息
图2.3-2 hello.i中部分类型定义信息
- 函数声明信息
图2.3-3 hello.i中部分函数声明信息
- 源码部分
图2.3-4 hello.i中源码部分
2.4 本章小结
在本章中,我们对hello.c进行了预处理,生成hello.i,预处理器会进行宏展开、头文件展开、条件编译等处理,并删除注释,对函数源码并不做过多修改,hello.i文件可用于下一步的处理。
第3章 编译
3.1 编译的概念与作用
3.1.1 编译的概念
编译是指预处理后,编译器(ccl)将预处理文件hello.i翻译成汇编语言文件hello.s。
3.1.2 编译的作用
- 语法检查:检查代码是否存在语法错误,如果有错误的话就会报错。
- 生成汇编代码:将程序翻译成汇编语言,从而在下一阶段可以让汇编器翻译成机器语言指令。
- 代码优化:编译器会对程序进行优化,生成效率更高的目标代码。
3.2 在Ubuntu下编译的命令
编译命令:gcc -S hello.i -o hello.s
-
图3.2-1 编译命令
3.3 Hello的编译结果解析
3.3.1 汇编代码展示
- .file "hello.c"
- .text
- .section .rodata
- .align 8
- .LC0:
- .string "\\347\\224\\250\\346\\263\\225: Hello \\345\\255\\246\\345\\217\\267 \\345\\247\\223\\345\\220\\215 \\347\\247\\222\\346\\225\\260\\357\\274\\201"
- .LC1:
- .string "Hello %s %s\\n"
- .text
- .globl main
- .type main, @function
- 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
- .L2:
- movl $0, -4(%rbp)
- jmp .L3
- .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
- movq -32(%rbp), %rax
- addq $24, %rax
- movq (%rax), %rax
- movq %rax, %rdi
- call atoi@PLT
- movl %eax, %edi
- call sleep@PLT
- addl $1, -4(%rbp)
- .L3:
- cmpl $8, -4(%rbp)
- jle .L4
- call getchar@PLT
- movl $0, %eax
- leave
- .cfi_def_cfa 7, 8
- ret
- .cfi_endproc
- .LFE6:
- .size main, .-main
- .ident "GCC: (Ubuntu 9.4.0-1ubuntu1~20.04.1) 9.4.0"
- .section .note.GNU-stack,"",@progbits
- .section .note.gnu.property,"a"
- .align 8
- .long 1f - 0f
- .long 4f - 1f
- .long 5
- 0:
- .string "GNU"
- 1:
- .align 8
- .long 0xc0000002
- .long 3f - 2f
- 2:
- .long 0x3
- 3:
- .align 8
- 4:
3.3.2 汇编文本结构分析
表3.2.2-1 hello.s文件结构
内容
含义
.file
源文件
.text
代码段
.global
全局变量
.data
存放已经初始化的全局和静态C 变量
.section .rodata
存放只读变量
.align
对齐方式
.type
表示是函数类型/对象类型
.size
表示大小
.long .string
表示是long类型/string类型
3.3.3 常量
编译时对常量进行编码,并将其存储在只读代码区的 .rodata节,在程序运行时会直接通过寻址找到常量。
例如将hello.c中“Usage: Hello 学号 姓名 秒数!”编译为汇编代码第6行:
- .string "\\347\\224\\250\\346\\263\\225: Hello \\345\\255\\246\\345\\217\\267 \\345\\247\\223\\345\\220\\215 \\347\\247\\222\\346\\225\\260\\357\\274\\201"
3.3.4 变量
不同类型的变量在不同位置定义,初始化的全局变量和静态变量定义在只读代码区的.bss节,已初始化的全局和静态变量定义在只读代码区的.data节,局部变量在堆上进行定义和释放。
例如,局部变量int i被保存在栈上,通过机器指令对其赋值
- movl $0, -4(%rbp)
3.3.5 赋值操作
对局部变量进行赋值操作,使用MOV指令,根据不同的数据大小选择不同指令movb、movw、movl、movq等。具体见3.3.4例。
3.3.5 算术运算
表3.3.5-1 算数指令
指令
效果
leaq s,d
d=&s
inc d
d+=1
dec d
d-=1
neg d
d=-d
add s,d
d=d+s
sub s,d
d=d-s
imulq s
r[%rdx]:r[%rax]=s*r[%rax]
mulq s
r[%rdx]:r[%rax]=s*r[%rax]
idivq s
r[%rdx]=r[%rdx]:r[%rax] mod s
r[%rax]=r[%rdx]:r[%rax] div s
divq s
r[%rdx]=r[%rdx]:r[%rax] mod s
r[%rax]=r[%rdx]:r[%rax] div s
在hello.s中,例如,实现i++的操作:
- addl $1, -4(%rbp)
开辟栈以及回收栈:
- subq $32, %rsp
3.3.6 比较和跳转操作
通过COM指令进行比较,计算两个值相减大小,根据结果设置条件码,根据条件码来判断跳转值,也可通过跳转指令J判断有无符号。
例如,检查argc是否不等于4。在hello.s中,使用cmpl $4,-20(%rbp),比较 argc与4的大小并设置条件码,为下一步je利用条件码进行跳转作准备。
- cmpl $4, -20(%rbp)
- je .L2
表3.3.6-1 跳转指令
指令
条件
jmp
直接跳转
je
相等
Jne
不等于
Js
小于
Jns
小于等于
Jg
大于
Jge
大于等于
Ja
大于(无符号)
Jae
大于等于(无符号)
jbe
小于等于(无符号)
3.3.7 数组/指针操作
对数组的索引相当于在第一个元素地址的基础上通过加索引值乘以数据大小来实现。
例如,在hello.c中,存在char *argv[],根据图3-3可知,根据argv首地址获得argv[1]和argv[2]需要通过加减操作:
- 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.8 函数操作
hello.c中包括main函数,printf函数,sleep函数,getchar函数,exit函数。
首先,内核shell获取命令行参数和环境变量地址,执行main函数,在main中需要调用其它函数,在main中为被调用函数分配栈空间。调用函数需要借助栈,先将返回地址压入栈中,并将PC设为被调用函数的起始地址,然后调用。返回时,先从栈中弹出返回地址,再PC设置为该返回地址。return正常返回后,leave恢复栈空间。
在hello.s中调用函数有:
call puts@PLT
call exit@PLT
call printf@PLT
call sleep@PLT
call getchar@PLT
3.4 本章小结
本章主要探讨编译器将经过预处理阶段后的C程序hello.i翻译成汇编语言程序的处理过程,包括对数据、算术操作、关系操作、控制转移、数组操作、函数操作的处理。编译器也会在处理过程中对程序进行一些优化,最终的结果被保存在hello.s文件中,能够在下一阶段让汇编器翻译机器语言指令。
第4章 汇编
4.1 汇编的概念与作用
4.1.1 汇编的概念
汇编是指汇编器(as)将hello.s翻译成机器语言指令的过程,把这些指令打包成可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
4.1.2 汇编的作用
将汇编指令转换成机器可以直接读取分析的机器指令,生成hello.o文件,用于后续的链接。
4.2 在Ubuntu下汇编的命令
汇编命令:gcc -m64 -no-pie -fno-PIC -c hello.s -o hello.o
-
图4.2-1 汇编命令
4.3 可重定位目标elf格式
在shell中输入readelf -a hello.o > hello.elf指令获得hello.o文件的 ELF 格式:
-
图4.3-1 生成elf格式文件
4.3.1 ELF头
以 16字节序列 Magic 开始,其描述了生成该文件的系统的字的大小和字节顺序,ELF 头剩下的部分包含帮助链接器语法分析和解释目标文件的信息,其中包括 ELF 头大小、目标文件类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量等相关信息。
-
图4.3.1-1 ELF头
4.3.2 节头
节头记录了各节名称及大小、类型及全体大小、地址及旗标、连接、信息和偏移量及对齐信息。
-
图4.3.2-1节头
4.3.3 重定位节
当链接器把这个目标文件和其他文件组合时,需要修改表中的这些位置。一般,调用外部函数或者引用全局变量的指令都需要修改。
其中包括R_X86_64_PC32( PC相对地址的引用)和R_X86_64_32(绝对地址的引用)。
图4.3.3-1重定位头
4.3.4 符号表
符号表中保存着定位、重定位程序中符号定义和引用的信息,所有重定位需要引用的符号都在其中声明。
-
图4.3.4-1符号表
4.4 Hello.o的结果解析
使用objdump -d -r hello.o > hello.asm分析hello.o的反汇编,并与第3章的 hello.s文件进行对照分析。
-
图4.4-1生成反汇编文件
4.4.1 反汇编代码
hello.o: 文件格式 elf64-x86-64- Disassembly of section .text:
- 0000000000000000 <main>:
- 0: f3 0f 1e fa endbr64
- 4: 55 push %rbp
- 5: 48 89 e5 mov %rsp,%rbp
- 8: 48 83 ec 20 sub $0x20,%rsp
- c: 89 7d ec mov %edi,-0x14(%rbp)
- f: 48 89 75 e0 mov %rsi,-0x20(%rbp)
- 13: 83 7d ec 04 cmpl $0x4,-0x14(%rbp)
- 17: 74 16 je 2f <main+0x2f>
- 19: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # 20 <main+0x20>
- 1c: R_X86_64_PC32 .rodata-0x4
- 20: e8 00 00 00 00 callq 25 <main+0x25>
- 21: R_X86_64_PLT32 puts-0x4
- 25: bf 01 00 00 00 mov $0x1,%edi
- 2a: e8 00 00 00 00 callq 2f <main+0x2f>
- 2b: R_X86_64_PLT32 exit-0x4
- 2f: c7 45 fc 00 00 00 00 movl $0x0,-0x4(%rbp)
- 36: eb 48 jmp 80 <main+0x80>
- 38: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 3c: 48 83 c0 10 add $0x10,%rax
- 40: 48 8b 10 mov (%rax),%rdx
- 43: 48 8b 45 e0 mov -0x20(%rbp),%rax
- 47: 48 83 c0 08 add $0x8,%rax
- 4b: 48 8b 00 mov (%rax),%rax
- 4e: 48 89 c6 mov %rax,%rsi<
HIT计统大作业——程序人生
计算机系统
大作业
题 目 程序人生-Hello’s P2P
专 业 计算机科学与技术
学 号 2021113008
班 级 2103102
学 生 石隆泰
指 导 教 师 刘宏伟
计算机科学与技术学院
2022年5月
多少人的代码生涯,都是从那一句“Hello World!”开始的?多少菜鸟手敲的第一个程序就是hello?可渐渐的,随着程序猿们知识越加丰厚,曾经的伙伴hello早已被抛之脑后。但是CS知道它的生,知道它的死,知道它来过。本文通过一个入门级程序hello.c,来剖析它坎坷的一生:从源代码到经过预处理、编译、汇编、链接最终生成可执行目标文件hello,再通过在shell 中键入启动命令后,shell 为其fork,产生子进程,内核为新进程创建数据结构, hello从可执行程序变成为进程。
关键词:Hello;预处理;编译;汇编;链接;进程
目 录
第1章 概述1.1 Hello简介
根据Hello的自白,利用计算机系统的术语,简述Hello的P2P,020的整个过程。
P2P(From Program to Process):用户通过编辑器编写人类可读的高级语言代码,得到一个hello.c程序(program)。为了在系统上运行hello程序,首先通过预处理器(cpp)得到修改了的源程序——ASCII码的中间文件hello.i,然后利用编译器(ccl)得到汇编程序——ASCII汇编语言文件hello.s,随后,再利用汇编器(as)生成可重定位目标程序hello.o(二进制)。接下来,经过链接器(ld),将调用的标准C库中的函数(如printf等)对应的预编译好了的目标文件以某种方式合并到hello.o文件中,得到可执行目标程序hello。此时即可把可执行文件加载到内存中,由系统执行,即成为一个进程(process)。
图1:编译系统
020(From Zero-0 to Zero-0):当输入“./hello”后,shell调用fork()函数创建子进程,子进程通过execve加载并执行该程序时,映射虚拟内存,程序开始时载入物理内存,进入CPU处理。CPU为执行文件hello分配时间片,进行取指、译码、执行等流水线操作。内存管理器和CPU在执行过程中通过L1、L2、L3三级缓存和TLB多级页表在物理内存中取的数据,通过I\\O系统根据代码指令进行输出。程序运行结束后,shell父进程会回收这个僵死进程,内核会从把它从系统内清除,这是hello由0转换为0,完成020的过程。
1.2 环境与工具
硬件环境
X64 CPU;3GHz;16G RAM;256GHD Disk 以上;
软件环境
Windows10 64位;VirtualBox/Vmware 16;kali linux;
开发工具
Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 20.04 LTS 64位;edb;gdb;gcc;ld;objdump;readelf;HexEdit。
1.3 中间结果
hello.c 源程序:能够被人读懂的c语言程序,以字节序列的方式储存
hello.i 预处理后文件:读取了相关头文件的内容
hello.s 编译后的汇编文件:包含了用汇编语言表示的main函数定义
hello.o 汇编后的可重定位目标执行文件:可被链接以待后续加载执行
hello 链接后的可执行文件:可以被加载到内存中由系统执行
elf.txt hello.o的ELF
hello1.elf hello的ELF
hello_asm.txt hello的反汇编文件
1.4 本章小结
本章简单介绍了Hello的P2P,020的过程,随后介绍了本文用到的环境与工具,以及撰写本文之后会生成的中间结果文件,为下文做铺垫。
第2章 预处理2.1 预处理的概念与作用
概念:预处理是指预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第一行中的 #include <stdio.h> 命令告诉预处理器读取系统头文件stdio.h的内容,并把它直接插入程序文本中。结果是得到了另一个C程序,通常以.i作为文件扩展名。
作用:主要作用有宏定义、文件包含、条件编译。
宏定义:将所有的#define删除,并且展开所有的宏定义,将宏名替换为文本。.
文件包含:预处理程序中的#include,将头文件的内容插入到该命令所在的位置,从而把头文件和当前源文件连接成一个源文件。
条件编译:处理所有条件预编译指令,根据#if以及#endif和#ifdef以及#ifndef来判断执行编译的条件。
2.2在Ubuntu下预处理的命令
Ubuntu下预处理的命令是:cpp hello.c > hello.i 或者 gcc -E hello.c -o hello.i
图2:Ubuntu下预处理的命令
2.3 Hello的预处理结果解析
打开hello.i后,发现原本普普通通仅仅四行的代码被拓展到了3000多行。比较hello.i的尾部文件和hello.c,会发现“#”宏定义消失了。事实上,预处理是遵从对于资源的等价交换,占据hello.i大部分空间的代码实际上是对头文件的展开,对引用目录的标注,诸如”/usr/include/stdio.h”。
接着比较尾部文件和.c文件会发现,除上述变化以外,函数主体部分并没有改变。
图3:hello.i与hello.c的内容对比
2.4 本章小结
本章主要围绕预处理进行介绍,在明白预处理概念与作用之后学习Ubuntu下预处理的命令。并以hello.c为例操作生成hello.i。打开解析hello.i文件,对比原.c文件发现不同,加深了对预处理的理解。
第3章 编译3.1 编译的概念与作用
概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序.该程序包含函数main的定义,这个过程称为编译。
作用:将高级语言源程序翻译成等价的目标程序,并且进行语法检查、调试措施、修改手段、覆盖处理、目标程序优化等步骤。此阶段编译器会完成对代码的语法和语义的分析,生成汇编代码,并将这个代码保存在hello.s文件中。
3.2 在Ubuntu下编译的命令
Ubuntu下编译的命令是:cc1 hello.i -o hello.s 或者gcc -S hello.i -o hello.s
图4:Ubuntu下编译的命令
3.3 Hello的编译结果解析
3.3.1文件声明
图5:hello.s头部文件
所有以‘.’开头的行都是指导汇编器和链接器工作的伪指令。
.file:声明源文件
.text:代码节
.rodata:只读代码段
.align 8:数据或者指令的地址对齐方式
.string:声明一个字符串(.LC0,.LC1)
.global:声明全局变量(main)
.type:声明一个符号是数据类型还是函数类型
3.3.2数据
(1)整型数据
图6:main()的参数传递
如图6可见main()函数的两个参数。
(2)字符串与立即数
图7:hello.s一部分头部文件
如图7,源程序中的两个字符串信息都被存储在.rodata段,而立即数会直接作为代码的一部分保存在.text section中。
(3)局部变量
图8:局部变量保存
如图8,int i当前被保存在-4(%rbp)中,由图5可知%rbp是指向栈的寄存器,所以i被保存在栈上,其在初始化时是被保存在寄存器中的。
3.3.3赋值操作
图9:赋值操作
赋值操作主要有mov指令实现,有以下几种:
movb:一个字节
movw:两个字节
movl:四个字节
movq:八个字节、
如图9,i=0这条赋值操作就是利用的movl指令来实现。
3.3.4算术操作
图10:算术运算
汇编中有add,sub,imul,xor等算术操作对应着c语言程序中的+、-、*、异或等操作,在hello源程序中有对循环变量进行+1的操作,对应到汇编中是通过addl指令完成的,如图10的第二行和最后一行。
3.3.5控制转移
图11:控制转移
其中cmpl比较了立即数4与-20(%rbp)处值的大小,为一个关系操作其会通过一次计算来设置条件码,而je通过前一次操作设置的条件码实现当-20(%rbp)中的值也就是第一个参数argc不等于4时,跳转到.L2处,其为控制转移。
3.3.6关系操作
图12:关系操作
cmpl同样比较了立即数7与-4(%rbp)中值的大小并设置条件码,根据条件码判断,当-4(%rbp)中的值小于等于7时,程序会跳转到.L4处,即C程序中的i<8时执行for循环中的内容。
3.3.7指针数组
图13:指针数组
如图13,main的第二个参数*argv[]是一个指针数组的首地址,它被保存在了寄存器%rsi中。
3.3.8函数操作
在hello.s中涉及的函数操作有:
main函数,printf,exit,sleep ,getchar函数;
main函数的参数是argc和argv;两次printf函数的参数恰好是那两个字符串;
exit参数是1,sleep函数参数是atoi(argv[3]);
函数的返回值存储在%eax寄存器中。
图14:if中printf与exit函数调用
图15:主循环中printf函数调用
图16:atoi函数调用
hello.c中涉及的类型转换函数是:atoi(),将字符串类型转换为整数类。
图17:sleep函数调用
图18:getchar函数调用
3.4 本章小结
本章主要介绍了编译的的概念以及编译的作用与功能,以及在Ubuntu下将hello.i文件编译生成hello.s文件的命令。
按照C语言的不同数据与操作类型,分析了源程序hello.c文件中的语句是怎样转化为hello.s文件中的语句的。其中数据类型包括数字常量、字符串常量和局部变量;操作类型包括赋值、类型转换、算术操作、关系操作、数组\\指针\\结构操作控制转移以及函数调用。
第4章 汇编4.1 汇编的概念与作用
概念:汇编是指经过编译后,汇编器as将汇编语言翻译成机器语言,得到可重定位目标文件的过程。
作用:将汇编进一步翻译为计算机可以理解的二进制机器语言,这是机器可直接识别执行的代码文件。
4.2 在Ubuntu下汇编的命令
Ubuntu下汇编的命令 as hello.s -o hello.o 或者 gcc -c hello.s -o hello.o
图19:Ubuntu下的汇编命令
4.3 可重定位目标elf格式
图20:hello.o的elf头
ELF首先由一个16B的magic数,该数描述了生成该文件的系统的字的大小和字节顺序,当魔数出现异常时,操作系统会停止加载该程序。ELF头剩下的部分包含帮助链接器语法分析和解释目标文件的信息:包括ELF头的大小、目标文件的类型、机器类型、节头部表的文件偏移,以及节头部表中条目的大小和数量。
图21:ELF节头部表
不同节的位置和大小是由节头部表描述的,其中目标文件中每个节都有一个固定大小的条目。节头记录每个节的名称、偏移量、大小、位置等信息。
.text节:已编译程序的机器代码以编译的机器代码。
.rela.text节:一个.text节中的列表,当链接器把这个目标文件和其他文件组合时,需要修改这些位置。
.data节:已初始化的静态和全局C变量。
.bss节:未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量,在目标文件中这个节不占据实际的空间,它仅仅是一个占位符。
.rodata节:存放只读数据。
.comment节:包含版本控制信息。
.symtab:一个符号表,存放在程序中定义和引用的函数和全局变量的信息。
.strtab节:一个字符串表,其内容包括.symtab和.debug节中的符号表,以及节头部的节名字。
.shstrtab节:该区域包含节的名称。
图22:重定位节
在.rela.text节中存放着代码的重定位条目。当链接器把这个目标文件和其他文件进行链接时,会结合这个节,修改.text节中相应位置的信息。
而重定位条目常见共2种:
R_X86_64_32:重定位绝对引用。重定位时使用一个32位的绝对地址的引用,通过绝对寻址,CPU直接使用在指令中编码的32位值作为有效地址,不需要进一步修改。
R_X86_64_PC32:重定位PC相对引用。重定位时使用一个32位PC相对地址的引用。一个PC相对地址就是据程序计数器的当前运行值的偏移量。
可以看出,对于字符串的都是绝对引用。每个重定位条目包含如下信息:该节包括的内容是:偏移量,信息,类型,符号值,符名称和加数。
图23:符号表
“.symtab”是一个符号表,它给出了存放在程序中定义和引用的函数和全局变量的信息,但是它不会保存本地非静态数据,因为这些数据会在程序运行的时候被保存在栈或者寄存器中。在进行符号解析的时候需要用到符号表。
4.4 Hello.o的结果解析
图24:hello.o反汇编内容
对比hello.o的反汇编代码,与hello.s的内容进行比较,发现以下几点的不同:
- 数字进制不同
hello.s中操作数都是十进制的,而在反汇编代码中以十六进制表示,这对应着在机器中以二进制的形式存在。
- 对字符串常量的引用不同
hello.s中是用的全局变量所在的那一段的名称加上%rip的值,而hello.o中用的是0加%rip的值,因为当前为可重定位目标文件,之后还需经过重定位方可确定其具体位置,所以这里都用0来代替。
- 分支转移不同
hello.s中列出了每个段的段名,分支转移时,跳转指令后用对应的段的名称表示跳转位置;而在hello.o的反汇编代码中每个段都有明确的地址,跳转指令后用相应的地址表示跳转位置。
- 函数调用不同
在hello.s中调用函数时,在call指令之后直接引用函数名称,而在hello.o的反汇编代码中,在call指令后加上下一条指令的地址来表示。观察机器语言,发现其中操作数都为0,这是因为通过重定位信息,再链接生成可执行文件后才会生成其确定的地址,所以这里的相对地址都用0代替。
4.5 本章小结
本章介绍了汇编的含义与作用,将hello.o转化成elf格式,主要分析了ELF头、节头部表、重定位节和符号表这几个节。之后通过objdump进行hello.o的反汇编与其hello.s进行对比,发现了其结构大致相同,但有些许差别,反汇编代码更加接近于底层,而汇编代码可读性更强。
第5章 链接5.1 链接的概念与作用
概念:链接是将各种代码和数据片段收集并组合成一个文件的过程,这个文件可被加载到内存执行。链接可以执行于编译时、加载时、运行时。
作用:把预编译好了的若干目标文件合并成为一个可执行目标文件。使得分离编译成为可能,不用将一个大型的应用程序组织为一个巨大的源文件,而是可以把它分解为可独立修改和编译的模块。
5.2 在Ubuntu下链接的命令
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
图25:Ubuntu下链接的命令
5.3 可执行目标文件hello的格式
图26:hello的ELF头
图27:hello的节头部表
图28:hello的程序头部表
图29:hello的重定位节内容
5.4 hello的虚拟地址空间
图30:edb打开hello的Data Dump窗口
edb的Data Dump窗口。窗口显示虚拟地址由0x400000开始,从开始到结束这之间的每一个节对应5.3中的每一个节头表的声明。
图31:edb打开hello的Loaded Symbols窗口
证实从虚拟地址从0x400000开始和5.3节中的节头表是一一对应的。
5.5 链接的重定位过程分析
运用指令objdump -d -r hello > hello-asm.txt得到hello的反汇编文件。
图32:反汇编的命令
将hello.o与hello文件的反汇编代码进行对比分析,得到如下几方面的不同:
- 链接后函数数量增加。多出了puts@plt,printf@plt,getchar@plt,exit@plt,sleep@plt等函数的代码。
图33:反汇编文件部分内容(一)
- 函数调用指令call的参数发生变化。
图34:反汇编文件部分内容(二)
- 跳转指令参数发生变化。
图35:反汇编文件部分内容(三)
5.6 hello的执行流程
5.7 Hello的动态链接分析
当程序调用一个由共享库定义的函数时,编译器无法预测这个函数运行时的地址,因为定义它的共享模块在运行时可以加载到任何位置。这时,编译系统提供了延迟绑定的方法,将过程地址的绑定推迟到第一次调用该过程时。他通过GOT和过程链接表PLT的协作来解析函数的地址。在加载时,动态链接器会重定位GOT中的每个条目,使它包含正确的绝对地址,而PLT中的每个函数负责调用不同函数。
在dl_init调用之前,对于每一条PIC函数调用,调用的目标地址都实际指向PLT中的代码逻辑,GOT存放的是PLT中函数调用指令的下一条指令地址。
通过节头表找到GOT起始位置0x404000
图36:got.plt起始位置
调用dl_init之前的情况:
图37:调用dl_init之前的内容
调用dl_init之后的情况:
图38:调用dl_init之后的内容
5.8 本章小结
本章结合实验中的hello可执行程序依此介绍了链接的概念及作用以及命令,并对hello的elf格式进行了详细的分析对比。以及hello的虚拟地址空间知识,并通过反汇编hello文件,将其与hello.o反汇编文件对比,详细了解了重定位过程,遍历了整个hello的执行过程,整理了过程中的子函数,在最后对hello进行了动态链接分析,对链接有了更深的理解。
第6章 hello进程管理6.1 进程的概念与作用
概念:进程就是一个执行中程序的实例。系统中的每个程序都运行在某个进程的上下文中。上下文是由程序正确运行所需的状态组成的。这个状态包括存放在内存中的程序的代码和数据,它的栈、通用目的寄存器地内容、程序计数器、环境变量以及打开文件描述符的集合。
作用:进程提供独立的逻辑控制流,好像我们的程序独占地使用处理器;也提供一个私有的地址空间,好像我们的程序独占地使用内存系统;使CPU被科学有效地划分成多个部分以并行地运行多个进程。
6.2 简述壳Shell-bash的作用与处理流程
作用:Shell为用户提供命令行界面,使用户可以在这个界面中输入shell命令,然后shell执行一系列的读/求值步骤,读步骤读取用户的输入的命令行,求值步骤则解析命令行,并运行程序。完成后重复上述步骤,直到用户退出shell。从而完成用户与计算机的交互来操作计算机。
处理流程:Shell打印一个命令行提示符,等待用户输入指令。在用户输入指令后,从终端读取该命令并进行解析,若该命令为shell的内置命令,则立即执行该命令;若不是内置命令,是一个可执行目标文件,则shell创建会通过fork创建一个子进程,并通过execve加载并运行该可执行目标文件,用waitpid命令等待执行结束后对其进行回收,从内核中将其删除;若将该文件转到后台运行,则shell返回到循环的顶部,等待下一个命令行。完成上述过程后,shell重复上述过程,直到用户退出shell。
6.3 Hello的fork进程创建过程
终端程序通过调用fork()函数创建一个子进程,子进程得到与父进程完全相同但是独立的一个副本,包括代码段、段、数据段、共享库以及用户栈。子进程还获得与父进程任何打开文件描述符相同的副本,父进程和子进程最大的不同时他们的PID是不同的。父进程与子进程是并发运行的独立进程,内核能够以任意方式交替执行它们的逻辑控制流的指令。在子进程执行期间,父进程默认选项是显示等待子进程的完成。
图39:创建子程序
以 ./hello 2021113008 shilongtai 1 为例,首先shell对我们输入的命令进行解析,由于我们输入的命令不是一个内置的shell命令,因此shell会调用fork()创建一个子进程,如图39。
6.4 Hello的execve过程
execve函数在当前进程的上下文中加载并运行一个新程序。
execve函数加载并运行可执行目标文件filename,且带参数列表argv和环境遍历列表envp。只有当出现错误时,execve才会返回到调用程序。所以,execve调用一次并从不返回。在execve加载了filename后,调用启动代码,启动代码设置栈,并将控制转移传递给新程序的主函数。
子进程通过execve函数系统调用启动加载器。加载器删除子进程现有的虚拟内存段,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片,新的代码和数据段被初始化为可执行文件的内容。最后加载器调到_start处,最终调用应用程序的main函数。
6.5 Hello的进程执行
当开始运行hello时,内存为hello分配时间片,如一个系统运行着多个进程,那么处理器的一个物理控制流就被分成了多个逻辑控制流,逻辑流的执行是交错的,它们轮流使用处理器,会存在并发执行的现象。其中,一个进程执行它的控制流的一部分的每一时间段叫做时间片。然后在用户态下执行并保存上下文。
如果在此期间内发生了异常或系统中断,则内核会休眠该进程,并在核心态中进行上下文切换,控制将交付给其他进程。
当hello 执行到 sleep时,hello 会休眠,再次上下文切换,控制交付给其他进程,一段时间后再次上下文切换,恢复hello在休眠前的上下文信息,控制权回到 hello 继续执行。
hello在循环后,程序调用 getchar() , hello 从用户态进入核心态,并再次上下文切换,控制交付给其他进程。最终,内核从其他进程回到 hello 进程,在return后进程结束。
6.6 hello的异常与信号处理
(以下格式自行编排,编辑时删除)
图40:异常的类别
hello执行过程中会出现哪几类异常,会产生哪些信号,又怎么处理的。
程序运行过程中可以按键盘,如不停乱按,包括回车,Ctrl-Z,Ctrl-C等,Ctrl-z后可以运行ps jobs pstree fg kill 等命令,请分别给出各命令及运行结截屏,说明异常与信号的处理。
图41:运行时不停乱按结果图
可以看到,虽然乱按不会影响程序的运行,但是会在程序运行结束后对shell发送许多无效指令,不过要注意因为hello程序最后有一个getchar,因此第一个乱按的指令被getchar给读走了,不会成为发送给shell的无效指令。因为我们可以判断,我们乱按的内容被放入缓冲区,等待程序执行结束被shell当作命令读走。
图42:运行时Ctrl-Z结果图
可以看到,ctrl+z后程序被放入后台并暂停运行。
图43:运行时Ctrl-C结果图
可以看到,ctrl+c后程序直接结束运行,回到shell等待输入下一条指令。
图44:Ctrl-Z后输入ps结果图
可以看到,输入ps,可以查看当前所有进程的相关信息,可以发现此时hello程序仍然存在。
图45:Ctrl-Z后输入jobs结果图
可以看到,输入jobs可以查看前台作业号。
图46:Ctrl-Z后输入pstree结果图
可以看到,输入pstree命令,以树形结构显示了程序和进程间的关系。
图47:Ctrl-Z后输入fg结果图可以看到,输入fg,能使被挂起的hello程序变成前台程序继续运行。
图48:Ctrl-Z后输入kill结果图
对比kill前后两次ps显示的进程信息,可以知道,输入kill -9 4488会将SIGKILL信号发送给进程4488,使得它被终止。
6.7本章小结
本章主要介绍了进程的概念与作用。同时介绍了壳Shell-bash作用与处理流程。明确了hello的fork进程创建过程与execve过程,通
以上是关于HIT-CSAPP程序人生大作业的主要内容,如果未能解决你的问题,请参考以下文章