<<程序员的自我修养>>第二章读书读书笔记----静态链接

        最近搞了一个Linux爱好者的微信订阅号,以Linux平台为主,定期也会分享一些网友的博文,和技术经验。因为网络文章,不和博主沟通,没法随意转发分享,如果您方便让您的经验一起分享,请您留下博客地址~~~以下是微信的订阅号,欢迎关注和讨论: 


        对一个不善写作的人来说,写一篇博客着实痛苦,但还是逼着自己去写,让所学的东西加深。本篇博客将对第二章内容进行回顾,理解,当然还要加上实践!

从C语言到可执行程序

        依稀记得第一次见到同学在TC编译器上运行一个“HelloWorld”的程序时的心情。不是惊叹,而是疑惑。写的代码比打印出来的“Hello World”字符串还要多很多,为啥不直接写在NotePad中显示出来?第一个问题好傻,是吧,各位看官笑笑就行。接下来我又问同学,“为什么你所写的代码能够运行后显示出来Hello World呢?”,那时候其实我对于“代码”,“运行”这些概念都不是很清楚。这个问题也就是本章内容所阐述的,我们一起来看看吧?

#include<stdio.h>
int main ()
{
         printf(“Hello World!”);
         return 0;
}

        上面是一段C语言的HelloWorld程序,C代码经过编译器编译之后就可以运行在操作系统之上了。这其中主要经历了4个阶段:预处理(Prepressing),编译(Compilation),汇编(Assembly)链接(Linking),如下图所示。


预编译

        预编译过程主要处理源代码文件中以”#”开始的预编译指令,主要规则如下:

  • 将”#define”删除,并展开所有的宏定义;
  • 处理所有条件预编译指令,比如”#if”, “#ifdef”, “#elif”, “else”, 和”#endif”;
  • 处理”#include”预编译指令,将包含的文件插入到预编译指令的位置;
  • 删除所有注释”//”和”/* */”;
  • 添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号;
  • 保留所有”#pragma”编译器指令,因为编译器需要使用他们。

扫描二维码关注公众号,回复: 3820585 查看本文章

        接下来,我们就来动手看一看,到底被预处理后的文件是什么个样子,首先我们写”hello.h”和”hello.c”:

//hello.h
#defineMACRO_TEST 5
 
/*
Function"add"
This function isused for add operation for two int type values.
*/
int add(intiVal1, int iVal2)
{
   return iVal1+iVal2;
}

//hello.c
#include<stdio.h>
#include"hello.h"
 
#ifdefMACRO_TEST
int iPlatform =1;
#else
int iPlatform =2;
#endif
 
#pragma pack(1)
 
int iVal =MACRO_TEST;
 
int main()
{
   //Just printf the values
   printf("iPlatform: %d, iVal %d, add%d\n", iPlatform, iVal, add(iPlatform, iVal));
   return 0;
}
 
然后在使用gcc对源码进行预编译处理:
[root@localhosttest]# gcc -E hello.c -o hello.i
在预处理后,可以看到结果如下所示(只取了部分):
# 2"hello.c" 2
# 1"hello.h" 1
 
int add(intiVal1, int iVal2)
{
   return iVal1+iVal2;
}
# 3"hello.c" 2
int iPlatform =1;
 
#pragma pack(1)
 
int iVal = 5;
 
int main()
{
 
   printf("iPlatform: %d, iVal %d, add%d\n", iPlatform, iVal, add(iPlatform, iVal));
   return 0;
}

         预处理的结果和之前所说的六条理论相一致,在自己开发过程中,用的最多的应该是去看宏扩展后的情况。有网友可能看到了[ # 3 "hello.c" 2], 其中3表示第三行,”hello.c”表示所在文件,”2”是跟在其后的flag,这里表示在”include”某个文件之后,再次回到这个文件,具体其他的falg,可以参考 PreprocessorOutput

编译

         很多时候,我们认为编译时指从源代码到可执行文件的过程,这里我们编译指:把预处理文件进行一系列词法分析语法分析语义分析优化后生成相应的汇编代码文件。

         使用gcc的命令进行反汇编:

 [root@localhosttest]# gcc -S hello.c -o hello.s

         反汇编的结果如下(只取了部分),汇编和我一样不怎么好的同学,也能明白个大概吧:

         main:
.LFB3:
       pushq   %rbp
.LCFI2:
       movq    %rsp, %rbp
.LCFI3:
       movl    iVal(%rip), %esi
       movl    iPlatform(%rip), %edi
       call    add
       movl    %eax, %ecx
       movl    iVal(%rip), %edx
       movl    iPlatform(%rip), %esi
       movl    $.LC0, %edi
       movl    $0, %eax
       call    printf
       movl    $0, %eax
       leave
       ret

        下面我们将根据如下代码,所经历的词法分析语法分析语义分析优化的过程进行分析:

         array[index]= (index + 4) * (2 + 6)

 词法分析

         学过编译原理课的都知道,这一步只需要通过一个有限状态机(Finite State Machine)来实现对代码的扫描,上面的代码将会被分成一个个的符号:”array”, “[“, “index”, “]”, “=”, “(“, “index”, “+”, “4”, “*”, “(“, “2”,“+”, “6”, “)” 。

         在词法分析的过程中,如果遇到非法的字符,则进行报错,退出编译过程。

 语法分析

         语法分析器采用了上下文无关语法(Context-free Grammar),这个是偏理论的东西,在这边我们暂且不做深入研究,可以看看相关的书籍。简单来说,由语法分析器生成的语法树就是以表达式为节点的数。以上的代码的语法树如下图所示:


         在语法分析阶段如果出现表达式不合法,比如括号不匹配、表达式缺少操作符等,编译器会报告语法分析阶段的错误。

 语义分析

         语法分析中只是将表达式已语法树的形式组织起来,但并没有对其进行含义的分析,即语义的分析,比如”*”表示两个变量或者值相乘,并且变量必须要能够进行乘法运算。下图为标示了语义的语法树:


我们在编译阶段最经常碰到的就是语义问题,比如类型不匹配。

 中间语言生成

         之前提到过代码优化,代码优化可分为两部分,一部分是编译器前端产生机器无关的中间代码,并进行优化;另一部分是,编译器后端将中间代码转换为目标机器代码,并进行优化。这一部分主要讲中间语言生成和优化。

         因为在语法树中直接对其进行优化比较困难(我想是树结构的操作吧?),所以往往先将语法树转换为中间代码。比较常见的中间代码类型有:三地址码(Three-addressCode)P-代码(P-Code). 采用三地址码描述之前的表达式如下:

t1 = 2 + 6
t2 = index + 4
t3 = t2 * t1
array[index] = t3

         优化后,即变成了:

t2 = index +4
t2 = t2 * 8
array[index] = t2

目标代码生成和优化

         中间代码,将根据不同的硬件平台,产生目标机器的能够执行的代码。注意这里的是能够执行的目标机器的代码,是进行汇编(Assembly)之后的结果,不过放在本章一起讲解一下。

         之前代码最终可能生成如下指令,这里使用x86汇编语言来描述:

movl index, %ecx          ; value of index to ecx
addl $4, %ecx             ; ecx = ecx + 4
mull $8, %ecx             ; ecx = ecx * 8
movl index, %eax          ; value of index to eax
movl %ecx, array(,eax,4)    ; array[index] = ecx

         8即为2的三次方,所以乘法可以采用移位的方式进行,则优化可以采用基指比例变址寻址(Base Index Scale Addressing)的lea指令来完成,优化后如下所示:

movl index, %ecx
leal 32(,%edx,8), %eax
movl %eax, arrray(,%edx,4)

         可能对AT&T汇编不熟悉的朋友,对上面的”array(,eax,4)”和”arrray(,%edx,4)”不是很理解。AT&T汇编形式”base(offset, index, i)”即为”base+offset+index*i”.

 汇编

         这里的汇编是指将编译过程中产生的汇编代码,转换为目标机器可以执行的指令。这个一个步骤比较简单,就将汇编代码对应机器指令一条一条的翻译。

         产生的hello.s可以用如下命令生成目标文件(ObjectFile)hello.o:

[root@localhost test]# gcc -c hello.s -ohello.o
         在上一章节中已经提到了目标代码生成后的优化。

 链接

         一个工程中一般由多个源文件构成,每一个源文件编译器处理过程中都会产生目标文件(C/C++中为.o文件)。这些目标文件以及静态库,动态库需要通过链接来完成(使用gcc或者ld命令去完成,其实gcc也是调用了ld^_^)。

         链接过程主要包括了地址和空间分配(Address and Storage Allocation)符号决议(Symbol Resolution)重定位(Relocation)等步骤。这些在后续的章节中会有详细的描述。

猜你喜欢

转载自blog.csdn.net/CJF_iceKing/article/details/47254641
今日推荐