一. 实验情况概述
本次实验完成了以下要求:
编写的一个简单C语言例子(至少包括3个C文件、至少一个头文件;含有初始化全局变量、未初始化全局变量、函数内静态变量与局部变量、赋值语句、if语句等),尽量不要包含第三方的头文件,按照预处理->编译->汇编->链接的操作过程,截图分析做成Word文档,理解并分析源程序与可执行程序Section的对应关系,特别是数据与指令,理解编译器的功能。
编写含有while循环、for循环的C程序,分析C语言的每个句子与汇编语言的对应关系,理解编译器的语义分析功能。
请编写含有函数调用(要有参数传递)的C程序,理解编译器在函数调用时所做的动作,并通过汇编试图去分析,验证函数的函数调用中的参数传递、调用与返回等
理解函数内局部变量如何分配,分析栈内分配过程。
编写的例子尝试编写一个makefile文件,然后进行make编译,然后通过make -j #n来编译,并验证加速编译。
二. 试验过程
一, 自己编写一个简单的C语言例子:
该例子包含四个文件,其中一个是头文件,具体如下:
图1.c语言源文件—main.c 图2 function.c
图3. yjy.h 图4 yjy.c
二, 分析编译过程:
使用如下命令进行预处理:
预处理后的文件内容如下:
图6 预处理后结果 main.i 图7 预处理后 yjy.i
图8 预处理后function.i
发现预处理的作用就是将头文件复制到.c文件中,并且将#define展开到文件使用的地方.
使用如下指令对文件进行编译:
图9 编译命令
编译后的文件内容为:
图10 编译后生成文件main.s 图11function.s
图12 yjy.s
由上图可知,编译的过程就是将经过宏展开的文件编译成汇编语言程序, 从而便于进一步汇编成为机器可以识别的机器语言程序.
图13 汇编命令
汇编生成机器语言程序,是二进制程序,所以无法打开查看内容.
使用objdump 指令来查看sections节:
可以发现0.是代码段,1.是数据段,2.是bss未初始化段,3.是drectve,……
代码段大小为0xfc,数据段为0x04,
代码段对应main 中所有语句的内容,包含局部变量的使用,函数调用等,数据段包含静态变量,对应我们定义的全局变量.
下面的symbol table 是符号表,只要包含section符号,函数符号等.
图14 链接命令
最后生成了可执行程序main.exe, 由于没有调用stdio.h,没有输入输出,所以程序执行完就退出了.
5. 分析可执行程序与源程序sections的对应关系:
使用如下指令查看可执行程序的sections:
使用如下指令查看可执行程序的sections:
结果如下:
可以看到包含很多节,其中代码段,是15,包含main 中的代码,数据段是24段,未初始化段bss是25段.
下面的符号表中定义了sections中的符号的含义,其他符号都定义了一段方法,可以在符号表中找到对应的内容.
三. 分析LLVM形式汇编程序
与高级语言程序 Section之间的对应关系:
在这里仅拿main.ll 举例说明:
使用如下指令进行编译成LLVM格式的中间代码;
图15 生成LLVM命令
结果如下:
现对上图进行分析:
开头四句话分别定义了:
模块ID,
源文件名,
目标数据层
目标编译类型
这些属于文件基本信息,不对应源文件位置.
这里定义了两个全局变量,int32类型,初始值分别为1,0, 4位大小对齐方式.
接下来这里定义了main函数, 第11~20句分配了局部变量, 都是int32类型,四位对齐方式,
第21~24句存储了部分局部变量的初始值,
然后25~26句 加载了函数参数到内存,第27句调用了add函数, 参数为前两句加载的,通过值传递方式, 第28句存储运算结果,这些整体对应
第29~31句对应, 语义分别为:加载%2的值到%14, %15=%14+1;
将%15的值存储到%2的位置, 这样就完成了n++的过程.
接下来32~35句就是调用multiply()的过程, 分别是参数写, 调用函数, 返回值 ,和上述add()类似,在此不做详解.
第36~38句对应,和上述类似.
第39~42句分别为:
参数传递, 比较 %21, %22的大小 ,将结果存入%23, 然后是跳转指令, 根据%23的值决定跳转到label %24 或者 %26. 对应.
之后是24和26两个标签,分别是加载%3到%25,或加载%4到%27然后无条件跳转到%28.
28标签中,有phi指令, phi节点是一个指令,用于根据当前块的前导选择一个值。也就是根据跳转的24或者26选择加载到%29的值到底是%25还是%27,这样就实现了maxn函数返回正确的值. 因为br指令判断后只会跳转一个标签,在这里选择返回哪个执行的值到%29实现正确的返回.然后再无条件跳转到30号标签.
此处我们已经开始了while的判断,30号标签正是判断内容的执行过程.
先加载%2到%31,然后比较判断%31 和 11 ,将结果存入%32,如果%31<11,则%32=0,然后吓一跳br根据%32的值选择跳转的位置.如果是0就跳转到33号标签,继续while内部的执行.
再然后就是33号标签对应while 语句内部的部分..
我们分析llvm的IR,发现最开始两条加载指令对应add函数的两个参数,然后call @add,然后存储返回结果,四条语句对应了一条函数执行的过程.
之后类似,又是四条语句对应了multiply的过程.之后三条语句对应了,这些语句都在前面解释过.
最后又无条件跳转到%30,继续判断是否n<11,然后再选择是否跳转到33继续执行while内的部分.
我们再看第42号lable:
调用fibonacci,调用FrogJumpSteps,定义equal,存储初始值为0,这些类似的都在上面解释过.第77~81行对应
第82~85行,先加载两个局部变量值,然后判断是否相等,然后再根据结果跳转到48或49.
这两个标签就对应给equal赋值赋哪个.然后再无条件跳转到50
50好标签就是返回0.
后面还有一些函数声明,变量声明等.
至此,分析结束
四, 编写makefile文件:
在wsl中使用time make编译, 单进程和多进程多次交叉进行,得到如下
尝试使用Makefile编译,结果如下:
多进程平均时间< 单进程平均时间,故可以得出结论 ,多进程加快了编译.