程序人生-Hello’sP2P-创新互联
P2P:From Program to Process,即程序从一个项目变成一个进程的过程。
做网站、成都网站设计,成都做网站公司-创新互联已向数千家企业提供了,网站设计,网站制作,网络营销等服务!设计与技术结合,多年网站推广经验,合理的价格为您打造企业品质网站。程序员通过键盘输入可得hello.c(program)c语言源程序,hello.c在预处理器(cpp)处理后得到hello.i,通过编译器(ccl),得到汇编程序hello.s,再通过汇编器(as),得到可重定位的目标程序hello.o,最后通过链接器(ld)得到可执行的目标程序hello(process)。
020:From Zero-0 to Zero-0,即程序从零开始又以零结束的过程。
在执行程序的过程中,操作系统为hello分配了虚拟内存,shell为hello创建了一个新的子进程,并在这个进程中调用execve函数,加载器将hello从磁盘中加载到内存,并将PC值设置为程序的入口,进入 main 函数执行目标代码。当程序运行结束后, shell 父进程负责回收 hello 进程,内核删除相关数据结构。
1.2 环境与工具 1.2.1硬件环境 X64 CPU;2GHz;2G RAM;256GHD Disk 以上 1.2.2 软件环境 Visual Studio 2010 64位以上;CodeBlocks 64位;vi/vim/gedit+gcc 1.2.3 开发工具 Windows7/10 64位以上;VirtualBox/Vmware 11以上;Ubuntu 16.04 LTS 64 位/优麒麟 64位 以上; 1.3 中间结果hello.c-程序员编写的c语言源程序
hello.i-预处理后的源程序
hello.s-编译后的汇编程序
hello.o-汇编后的可重定位的目标文件
hello-链接后的可执行文件
elf.txt- hello.o的elf格式文件
helloelf.txt- hello的elf格式文件
1.4 本章小结本节对hello的P2P和020过程进行了大致介绍,并给出了整个过程中所使用的环境和工具及生成的中间文件。
第2章 预处理 2.1 预处理的概念与作用预处理概念:预处理器(cpp)根据以字符#开头的命令,修改原始的C程序。比如hello.c中第1行的#include
预处理作用:将头文件的内容插入到程序文本中,方便编译器进行下一步的编译。
2.2在Ubuntu下预处理的命令命令:gcc -E hello.c -o hello.i
图2.2Linux下预处理结果
2.3 Hello的预处理结果解析通过分析hello.i文件内容,代码变成了3091行,其中main函数占3078-3091行,文件前面插入了被#include的系统头文件。
2.4 本章小结本节获取了预处理后的hello.i文件,并据其分析了预处理的过程。
第3章 编译 3.1 编译的概念与作用编译概念:编译器(ccl)将文本文件hello.i翻译成文本文件hello.s,它包含一个汇编语言程序,该程序包含函数main的定义。
编译作用:它为不同高级语言的不同编译器提供了通用的输出语言。
3.2 在Ubuntu下编译的命令命令:gcc -S hello.i -o hello.s
图3.2Linux下编译结果
3.3 Hello的编译结果解析3.3.1数据
(1)字符串
只读字符串,在.rodata中声明
(2)数组
Main函数读取参数时,argv作为存放 char指针的数组同时是第二个参数传入。
3.3.2赋值
使用mov指令,如图:
movl后缀代表数据大小为4个字节,将0赋值到%eax中。
3.3.3算术操作
使用算术操作指令进行算数操作,如图:
此处addl将%rbp-4地址处的值加了1。
3.3.4关系操作
使用 cmp指令进行比较,如图:
cmpl对应源代码i与8比较,jle代表i<=8时,跳转至L4循环体。
3.3.5函数操作
(1)参数传递
函数调用参数传递规则:第1~6个参数依次储存在%rdi、%rsi、%rdx、%rcx、%r8、%r9寄存器中,剩下的参数通过栈传递。
(2)函数调用
使用call指令调用函数,call后接函数名称,如图:
表示调用printf函数。
3.4 本章小结本节获取了编译后的hello.s文件,并据其分析了编译的过程。
第4章 汇编 4.1 汇编的概念与作用汇编概念:汇编器(as)将hello.s翻译成机器语言指令,把这些指令打包成一种叫做可重定位目标程序的格式,并将结果保存在目标文件hello.o中。
汇编作用:将文本文件翻译为机器可读懂的二进制文件,以便于执行链接。
4.2 在Ubuntu下汇编的命令gcc -c hello.s -o hello.o
图4.2Linux下汇编结果
4.3 可重定位目标elf格式指令:readelf -a hello.o >./elf.txt
4.3.1ELF头
包含信息为文件结构的说明信息:16字节的标识信息,文件类型,机器类型,节头表偏移,节头表的表项大小,表项个数,生成该文件的系统字大小和字节顺序
4.3.2节头部表
节头部表描述不同节的位置和大小信息,其中目标文件的每个节都有一个固定大小的条目。
4.3.3重定位节
包含重定位的信息,链接时链接器把这个目标文件和其他文件组合时,进行重定位并根据信息修改位置。
4.4 Hello.o的结果解析指令:objdump -d -r hello.o
操作数:hello.s中操作数为十进制,反汇编代码中操作数为十六进制。
分支转移:反汇编代码中的跳转指令不再使用段名称如L0、L1等,而是采用跳转到指定地址的方式进行跳转。
4.5 本章小结本节获取了汇编后的hello.o文件,并获得ELF格式,最后分析了汇编代码与反汇编代码的区别。
第5章 链接 5.1 链接的概念与作用链接概念: C编译器提供的标准C库中的函数存在于一个单独的预编译好了的目标文件中,链接器(ld)负责合并这个文件到我们的hello.o程序中。
链接作用:将独立的可重定位目标模块组织成统一的可执行目标文件。
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.2Linux下汇编结果
5.3 可执行目标文件hello的格式命令:readelf -a hello >helloelf.txt
5.3.1ELF头
5.3.2节头部表
从节头部表可以得到每个节的位置和大小信息,比如.dynsym节的开始位置是0x400480,大小为67个字节
5.3.3程序头
5.4 hello的虚拟地址空间图5.4Linux系统edb中的hello程序
分析:从data dump中初始位置为0x401000,与5.3节中程序的入口点.init节起始位置相同
5.5 链接最后分析了汇编代码与反汇编代码的重定位过程分析指令:objdump -d -r hello
图5.5 hello反汇编代码
分析:hello的反汇编代码加入了其他库函数(如printf),故代码量比hello.o长,而且由于hello无需再重定位,因此hello中的地址为虚拟内存地址。
链接过程中动态链接器为函数定义了程序入口_start、初始化函数_init,_start 程序调用main函数并链接库函数,链接器将这些函数链接到一起使程序能够最终执行。
重定位过程中链接器首先将所有相同类型的节合并成为同一类型的新节,合并完成后该新节即为可执行文件hello的.data节。之后链接器再分配内存地址赋给新的节和输入模块定义的节以及符号。
5.6 hello的执行流程使用edb执行hello,程序初始位置位于0x7f5caf5c22b0
1. 从加载到进入main函数的过程
开始时,经过一系列执行,程序首先跳转到子程序_start,该子程序位于地址4010f0处。随后通过callq *0x2ed2(%rip) 指令跳转到位于地址0x7f38faffefc0的“Libc-2.31.so!_libc_start_main”子程序,在子程序中,通过call *%rax指令跳转到main函数,地址为0x401125。
2. 从main函数到程序执行完
进入main函数,程序按照源代码顺序依次执行,在执行的过程中分别调用不同的子程序,子程序名称和地址如下:
401090
4010a0
4010b0
4010c0
4010d0
4010e0
首先,根据节头部表找到GOT表地址,由图可知其在0x403ff0。
在edb的data dump中定位0x403ff0地址
由结果可知在dl_init前后,0x403ff0 处和0x404000 处的8bit数据分别由000000000000变为了c07d74c57b7f 和e03299c57b7f,GOT[1]指向重定位表,作用是确定调用函数的地址,GOT[2]指向动态链接器ld-linux.so运行时地址
5.8 本章小结本节介绍了链接的概念和作用,同时分析了hello程序的虚拟地址空间、重定位过程、执行流程、动态链接过程。
第6章 hello进程管理 6.1 进程的概念与作用 进程概念:进程的经典定义就是一个执行中程序的实例,是操作系统对一个正在运行程序的抽象。 进程作用:进程提供给应用程序的关键抽象;一个独立的逻辑控制流,如同程序独占处理器;一个私有的地址空间,如同程序独占内存系统 6.2 简述壳Shell-bash的作用与处理流程作用:shell提供了一个界面,它解释用户输入的命令并将它送入内核。
处理流程:shell从终端读入指令,将输入的字符串切分获得全部参数,首先判断指令是否为内部指令,内部指令将被执行,否则检查是否为一个可执行文件。如果两者都不是,shell将显示一个错误信息。
6.3 Hello的fork进程创建过程执行./hello后,bash解析此条命令,发现./hello不是bash内置命令,于是在当前目录尝试寻找并执行hello文件。此时bash调用fork函数创建一个子进程,子进程与得到一份与父进程用户级虚拟空间相同且独立的副本——包括数据段、代码、共享库、堆和用户栈。二者间的PID不相同,fork函数会返回两次,在父进程中,返回子进程的PID,在子进程中,返回0。
6.4 Hello的execve过程execve开始执行hello有以下4个步骤:
1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。即删除之前shell运行时已经存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text 和.data区。bss区域是请求二进制零的,映射到匿名文件,其大小包含在hello中。栈和堆区域也是请求二进制零的,初始长度为零。
3.映射共享区域。hello程序与共享对象链接,比如标准C库1ibc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的人口点。
6.5 Hello的进程执行当我们让它运行hello程序时,shell通过系统调用,来执行我们的请求,系统调用会将控制权传递给操作系统。操作系统保存shell进程的上下文,创建一个新的hello进程及其上下文,然后将控制权传给新的hello进程。在hello进程的执行过程中,在某些时刻,hello程序调用sleep函数显式请求进程休眠,此时内核执行上下文切换,从用户模式转换到内核模式,抢占hello进程。随后,在其他进程执行一段时间后,内核做出决定将控制返回给hello进程继续执行。如此反复直到hello进程终止后,操作系统恢复shell进程的上下文,并将控制权传给它,shell进程会继续等待下一个命令行输入
6.6 hello的异常与信号处理异常:hello执行过程中会出现异步中断异常和系统调用,中断即来自外部I/O设备的信号打断进程。
信号:会产生诸如SIGINT,SIGQUIT,SIGTSTP等信号。
运行处理:
6.6.1不停乱按(包括回车)
只是在屏幕上显示出来并未对程序的输出造成影响
6.6.2输入CTRL+Z
向内核发送信号SINGINT,陷入到内核态,处理信号,该命令为将进程挂起并显示处理结果。
输入ps可查看进程
输入jobs列出jobs可知进程处于停止状态
输入pstree
输入fg继续前台进程
对比输入kill前后hello进程被杀死
6.6.3输入CTRL+C
向内核发送信号SINGINT,陷入到内核态,处理信号,终止程序
6.7本章小结本节从进程、异常、上下文的角度分析了hello的执行过程,以及在执行过程中程序对异常的处理流程。
第7章 hello的存储管理 7.1 hello的存储器地址空间逻辑地址: cpu执行程序过程中的一种中间地址。一个逻辑地址,是由一个段标识符加上一个指定段内的相对地址的偏移量。
线性地址:是逻辑地址到物理地址变换之间的中间层。程序代码会产生逻辑地址,或者说是段中的偏移地址,加上相应段的基地址就生成了一个线性地址。
虚拟地址:是相对于物理内存对整个内存的抽象描述。有了这样的抽象,一个程序可以使用比真实物理地址大得多的地址空间,多个进程可以使用相同的地址。
物理地址: 用于内存芯片级的单元寻址,与地址总线相对应。
7.2 Intel逻辑地址到线性地址的变换-段式管理 逻辑地址是程序源码编译后所形成的,跟实际内存没有直接联系的地址,即在不同的机器上,使用相同的编译器来编译同一个源程序,则其逻辑地址是相同的。 线性地址=段基址*16+偏移的逻辑地址,而段基址由于不同的机器其任务不同,其所分配的段基址(线性地址)也会不相同,因此,其线性地址会不同。 7.3 Hello的线性地址到物理地址的变换-页式管理线性地址对应到物理地址是通过分页机制,即通过页表查找来对应物理地址。
分页是CPU提供的一种机制,Linux根据这种机制的规则,利用它实现了内存管理。在保护模式下,控制寄存器的最高位PG位控制着分页管理机制是否生效,如果PG=1,分页机制生效,需通过页表查找才能把线性地址转换物理地址。
分页的基本原理是把内存划分成大小固定的若干单元,每个单元称为一页,每页包含4k字节的地址空间。这样每一页的起始地址都是4k字节对齐的。为了能转换成物理地址,我们需要给CPU提供当前任务的线性地址转物理地址的查找表,即页表。x86将线性地址通过页目录表和页表两级查找转换成物理地址。
7.4 TLB与四级页表支持下的VA到PA的变换一到三级页表中存放的数据是指向下一级页表的首地址,而不是物理页号。逐步访问到第四级页表,第四级页表中装的就是物理页号,通过第四级页表读出的物理页号链接上虚拟地址中的VPO获得物理地址。用页表进行虚实地址转化的基本原理如下图:
7.5 三级Cache支持下的物理内存访问首先是CPU发出一个虚拟地址给TLB里面搜索,如果命中的话就直接先发送到L1cache里面,没有命中的话就先在页表里面找到以后再发送过去,到了L1里面以后,寻找物理地址又要检测是否命中。
7.6 hello进程fork时的内存映射当fork 函数被shell调用时,内核为hello进程创建各种数据结构,并分配给它一个唯一的PID 。并且创建hello进程的mm_struct、区域结构和页表的原样副本。它将两个进程中的每个页面都标记为只读,并将两个进程中的每个区域结构都标记为私有的写时复制。当这两个进程中的任一一个后来进行写操作时,写时复制机制就会创建新页面,因此,也就为每个进程保持了私有地址空间的抽象概念。
7.7 hello进程execve时的内存映射1.删除已存在的用户区域。删除当前进程虚拟地址的用户部分中的已存在的区域结构。即删除之前shell运行时已经存在的区域结构。
2.映射私有区域。为hello的代码、数据、bss和栈区域创建新的区域结构。所有这些新的区域都是私有的、写时复制的。代码和数据区域被映射为hello文件中的.text 和.data区。
3.映射共享区域。hello程序与共享对象链接,比如标准C库1ibc.so,那么这些对象都是动态链接到这个程序的,然后再映射到用户虚拟地址空间中的共享区域内。
4.设置程序计数器(PC)。最后,execve设置当前进程上下文中的程序计数器,使之指向代码区域的人口点。
7.8 缺页故障与缺页中断处理在请求分页系统中,可以通过查询页表中来确定所要访问的页面是否存在于内存中。每当所要访问的页面不在内存时,会产生一次缺页中断,此时操作系统会根据页表中的外存地址在外存中找到所缺的一页,并将其调入内存。
处理:选择一个牺牲页面,如果这个牺牲页面被修改过,那么就将它换入新的页面并更新页表。当缺页处理程序返回时,CPU 重新启动引起缺页的指令。
7.9动态存储分配管理在程序运行时程序员使用动态内存分配器获得虚拟内存,动态内存分配器维护着一个进程的虚拟内存区域,称为堆。分配器将堆视为一组不同大小的块的集合,每个块要么是已分配的,要么是空闲的。分配器的类型有显示分配和隐式分配,前者要求应用显式地释放任何已分配的块,后者应用检测到已分配块不再被程序所使用,就释放这个块。
7.10本章小结本节分析了hello的存储器空间,并给出了 TLB与四级页表支持下的VA与 PA的变换和三级Cache支持下的物理内存访问过程的介绍。
第8章 hello的IO管理 8.1 Linux的IO设备管理方法设备的模型化:文件
设备管理:unix io接口
所有的I/ O 设备(例如网络、磁盘和终端)都被模型化为文件,而所有的输入和输出都被当作对相应文件的读和写来执行。这种将设备映射为文件的方式,允许Linux 内核引出一个简单、低级的应用接口,称为Unix I/O,这使得所有的输入和输出都能以一种统一且一致的方式来执行,这就是Unix I/O接口。
8.2 简述Unix IO接口及其函数Linux/unix IO接口:
- 打开文件:返回一个小的非负整数,即描述符。用描述符来标识文件。
- 改变当前文件位置 从文件开头起始的字节偏移量。系统内核保持一个文件位置k,对于每个打开的文件,起始值为0。
- 读写文件:一个读操作就是从文件复制n>0个字节到内存,从当前文件位置k开始,然后将k增加到k+n。写操作就是从内存复制n>0个字节到一个文件,从当前文件位置k开始,然后更新k。
- 关闭文件:当应用完成了对文件的访问之后,它就通知内核关闭这个文件。
函数:
Open()-打开一个已经存在的文件或是创建一个新文件
Read()-从文件读取数据,执行输出
Write()-从文件中读取数据,执行输出
Close()-关闭一个被打开的文件
Lseek()-用于在指定的文件描述符中将文件指针定位到相应位置
8.3 printf的实现分析printf()函数将变长参数的指针arg作为参数,传给vsprintf函数。然后vsprintf函数解析格式化字符串,调用write()函数。在write函数中,将栈中参数放入寄存器,ecx是字符个数,ebx存放第一个字符地址,最后,write函数调用syscall(int INT_VECTOR_SYS_CALL)。syscall将字符串中的字节从寄存器中通过总线复制到显卡的显存中,显存中存储的是字符的ASCII码。字符显示驱动子程序将通过 ASCII 码在字模库中找到点阵信息将点阵信息存 储到 vram 中。显示芯片会按照一定的刷新频率逐行读取 vram,并通过信号线向液晶显示器传输每一个点(RGB 分量),最终打印出了我们需要的字符串。
8.4 getchar的实现分析异步异常-键盘中断的处理:键盘中断处理子程序。接受按键扫描码转成ascii码,保存到系统的键盘缓冲区。
getchar等调用read系统函数,通过系统调用读取按键ascii码,直到接受到回车键才返回。
8.5本章小结本节主要介绍了 Linux 的 IO 设备管理方法、Unix IO 接口及其函数,简单分析了 printf 函数和 getchar 函数的实现。
结论hello程序所经历的过程:
1.编写:程序员通过键盘输入可得hello.c(program)c语言源程序。
2.预处理:hello.c在预处理器(cpp)处理后得到hello.i。
3.编译:通过编译器(ccl),得到汇编程序hello.s。
4.汇编:通过汇编器(as),得到可重定位的目标程序hello.o。
5.链接:通过链接器(ld)得到可执行的目标程序hello(process)。
6.运行:在shell命令行输入./hello 2021112660 孙吴锡 3运行程序。
7.创建子进程:shell调用fork函数创建子进程。
8.运行程序:调用execve将程序加载进去。
9.结束:shell父进程回收子进程,内核删除为这个进程创建的所有数据结构。
感悟:
即使最简单的一个hello程序,其执行处理的过程也是非常复杂的,要想真正理解他要花费很大的功夫,我从中学到了预处理、编译、汇编、链接、进程管理、存储管理、IO管理的相关知识,这次的课程让我获益匪浅。
附件列出所有的中间产物的文件名,并予以说明起作用。
hello.c-程序员编写的c语言源程序
hello.i-预处理后的源程序
hello.s-编译后的汇编程序
hello.o-汇编后的可重定位的目标文件
hello-链接后的可执行文件
elf.txt- hello.o的elf格式文件
helloelf.txt- hello的elf格式文件
参考文献[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.
你是否还在寻找稳定的海外服务器提供商?创新互联www.cdcxhl.cn海外机房具备T级流量清洗系统配攻击溯源,准确流量调度确保服务器高可用性,企业级服务器适合批量采购,新人活动首月15元起,快前往官网查看详情吧
文章名称:程序人生-Hello’sP2P-创新互联
文章位置:http://myzitong.com/article/issse.html