程序人生-Hello’s P2P
**
摘要
**
本文遍历了hello.c在Linux下生命周期,借助Linux下系列开发工具,通过对其预处理、编译、汇编等过程的分步解读及对比来学习各个过程在Linux下实现机制及原因。同时通过对hello在Shell中的动态链接、进程运行、内存管理、I/O管理等过程的探索来更深层次的理解Linux系统下的动态链接机制、存储层次结构、异常控制流、虚拟内存及UnixI/O等相关内容。旨在将课本知识与实例结合学习,更加深入地理解计算机系统的课程内容。
**
关键词
**
操作系统;编译;链接;虚拟内存;异常控制流;
1.1Hello简介
1.1.1 From Program to Process
首先hello.c通过I/O设备如键盘等经过总线存入主存。然后GCC编译器驱动程序读取源程序文件hello.c,通过预处理器cpp变成hello.i(修改了的源程序)然后通过编译器ccl变成hello.s(汇编程序),然后通过汇编器as变成hello.o(可重定位目标程序),这时的hello.o就不是之前的文本了,而是对机器友好的二进制代码了。最后再通过链接器ld与标准C库进行链接,最终变成hello(可执行的二进制目标程序)此时的hello就是一个Program了。然后在shell(Bash)里面输入字符串“./hello”后,shell程序将字符逐一读入寄存器,然后再放入到内存里面去,然后shell调用fork函数创建一个新运行的子进程,这个子进程是父进程shell的一个复制,然后子进程通过execve系统调用启动加载器。加载器删除子进程现有的虚拟内存段,然后使用mmap函数创建新的内存区域,并创建一组新的代码、数据、堆和栈段。新的栈和堆段被初始化为零。通过将虚拟地址空间中的页映射到可执行文件的页大小的片(chunk), 新的代码和数据段被初始化为可执行文件的内容。最后,加载器跳转到_start地址,它最终会调用应用程序的main 函数。然后程序从内存读取指令字节,然后再从寄存器读入最多两个数,然后在执行阶段算术/逻辑单元要么执行指令指明的操作,计算内存引用的有效地址要么增加或者减少栈指针。然后在流水线化的系统中,待执行的程序被分解成几个阶段,每个阶段完成指令执行的一部分。最后变成一个Process运行在内存中。
1.1.2 From Zero-0 to Zero-0
首先说明这里的020应该指的是程序(Process)在内存中From Zero to Zero。一开始hello先是执行了上面所述的过程,然后在程序执行结束以后,该进程会保持在一种已终止的状态中,直到该进程被其父进程也就是shell进程回收然后退出,shell会再次变成hello执行之前的状态,也就是说又变成Zero了。
1.2 环境与工具
硬件环境:Intel Core i7-6700HQ x64CPU,16G RAM,256G SSD +1T HDD;
软件环境:Ubuntu18.04.1 LTS;
开发与调试工具:vim,gcc,as,ld,edb,readelf,HexEdit;
1.3 中间结果
hello.c:源代码
hello.i:hello.c预处理生成的文本文件。
hello.s:hello.i经过编译器翻译成的文本文件hello.s,含汇编语言程序。
hello.o:hello.s经汇编器翻译成机器语言指令打包成的可重定位目标文件
hello.elf:hello.o的ELF格式。
hello_o_asm.txt:hello.o反汇编生成的代码。
hello:经过hello.o链接生成的可执行目标文件。
hello_out.elf:hello的ELF格式。
hello_out_asm.txt:hello反汇编生成的代码。
1.4 本章小结
本章主要是漫游式地了解hello在系统中生命周期,对每个部分需要有系统地了解;同时本章列出本次实验的基本信息。
第2章 预处理
2.1 预处理的概念与作用
概念:预处理器cpp根据以字符#开头的命令(宏定义、条件编译),修改原始的C程序,将引用的所有库展开合并成为一个完整的文本文件。
主要功能如下:
1、将源文件中用#include形式声明的文件复制到新的程序中。比如hello.c第6-8行中的#include<stdio.h> 等命令告诉预处理器读取系统头文件stdio.h unistd.h stdlib.h 的内容,并把它直接插入到程序文本中。
2、用实际值替换用#define定义的字符串。
3、根据#if后面的条件决定需要编译的代码。
2.2在Ubuntu下预处理的命令
gcc -E hello.c -o hello.i
2.3 Hello的预处理结果解析
在这之前出现的是stdio.h unistd.h stdlib.h的依次展开,以stdio.h的展开为例,cpp到默认的环境变量下寻找stdio.h,打开/usr/include/stdio.h 发现其中依然使用了#define语句,cpp对此递归展开,所以最终.i程序中是没有#define的。而且发现其中使用了大量的#ifdef #ifndef的语句,cpp会对条件值进行判断来决定是否执行包含其中的逻辑。其他类似。
2.4 本章小结
.c文件中包含有头文件也就是有外部文件的,还有一些程序员需要但是对于程序执行没有任何帮助的宏定义以注释,和一些程序员需要的条件编译和完善程序文本文件等操作都需要通过预处理来实现。预处理可以使得程序在后序的操作中不受阻碍,是非常重要的步骤。
第3章 编译
3.1 编译的概念与作用
概念: 编译是利用编译程序从预处理文本文件产生汇编程序(文本)的过程。主要包含五个阶段:词法分析;语法分析;语义检查、中间代码生成、目标代码生成。
作用:编译作用主要是将文本文件hello.i翻译成文本文件hello.s,并在出现语法错误时给出提示信息,执行过程主要从其中三个阶段进行分析:
- 词法分析。词法分析的任务是对由字符组成的单词进行处理,从左至右逐个字符地对源程序进行扫描,产生一个个的单词符号,把作为字符串的源程序改造成为单词符号串的中间程序;
- 语法分析。语法分析器以单词符号作为输入,分析单词符号串是否形成符合语法规则的语法单位,如表达式、赋值、循环等,最后看是否构成一个符合要求的程序,按该语言使用的语法规则分析检查每条语句是否有正确的逻辑结构,程序是最终的一个语法单位;
- 目标代码生成。目标代码生成器把语法分析后或优化后的中间代码经汇编程序汇编生成汇编语言代码,成为可执行的机器语言代码。
注意:这儿的编译是指从 .i 到 .s 即预处理后的文件到生成汇编语言程序
3.2 在Ubuntu下编译的命令
gcc -S -o hello.s hello.i
应截图,展示编译过程!
3.3 Hello的编译结果解析
3.3.1 数据
有变量int sleepsecs,编译器将其编译成
.type sleepsecs, @object
.size sleepsecs, 4
3.3.2 赋值
赋值语句sleepsecs=2.5编译器将其编译成
sleepsecs:
.long 2
.section .rodata
赋值语句i=0编译器将其编译成
movl $0, -4(%rbp)
3.3.3 类型转换(显示或隐式)
由于sleepsecs是int型的而2.5是float类型的,这就有一个隐式的类型转换,编译器将2.5隐式地转换成了2存入sleepsecs。
3.3.4 算术操作
编译器将i++编译成
addl $1, -4(%rbp)
3.3.5 关系操作
编译器将i<10编译成
cmpl $9, -4(%rbp)
jle .L4
将argc!=3编译成
cmpl $3, -20(%rbp)
je .L2
3.3.6 数组/指针/结构操作
printf函数里面的一系列对指针和对数组的操作编译器编译为:
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 控制转移
编译器将if,for等控制转移语句都使用了cmp来比较然后使用了条件跳转指令来跳转。编译器将if(argc!=3)编译成:
cmpl $3, -20(%rbp)
je .L2
将for循环里面的比较和转移编译成:
cmpl $9, -4(%rbp)
jle .L4
3.3.8 函数操作
编译器将printf(“Usage: Hello 学号 姓名!\n”);编译为:
movl $.LC0, %edi
call puts
将printf(“Hello %s %s\n”,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
movl $.LC1, %edi
movl $0, %eax
call printf
将sleep(sleepsecs);编译为:
movl sleepsecs(%rip), %eax
movl %eax, $edi
call sleep
此部分是重点,说明编译器是怎么处理C语言的各个数据类型以及各类操作的。应分3.3.1~ 3.3.x等按照类型和操作进行分析,只要hello.s中出