程序的奥秘-链接、装载和库-未完

        写了十来年的代码,总觉得缺点什么,最近看了《程序员的⾃我修养》,有不少收获,把里面一些重点东西记录下来。

入门

        对于很多程序员来讲,学习一门新的语言总是先写hello world,我们这篇也不例外,下面是一个标准C的hello world:

#include <stdio.h>
  
int  main(int argc, char **argv)
{
        printf("hello world\n");
        return 0;
}

面对上面的程序,大家肯定不会有什么感觉,不过对于下面的这些问题,你的脑⼦⾥能够马上反应出⼀个很清晰又很明确的答案吗?

? 程序为什么要被编译器编译了之后才可以运⾏?
? 编译器在把C语⾔程序转换成可以执⾏的机器码的过程中做了什么,怎么 做的?
? 最后编译出来的可执⾏⽂件⾥⾯是什么?除了机器码还有什么?它们怎 么存放的,怎么组织的?
? #include <stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语⾔库 又是什么?它怎么实现的?
? 不同的编译器(Microsoft VC、GCC)和不同的硬件平台(x86、 SPARC、MIPS、ARM),以及不同的操作系统(Windows、Linux、 UNIX、Solaris),最终编译出来的结果⼀样吗为      什么?? Hello World程序是怎么运⾏起来的?操作系统是怎么装载它的?它从哪 ⼉开始执⾏,到哪⼉结束?main函数之前发⽣了什么?main函数结束以后 又发⽣了什么?
? 如果没有操作系统,Hello World可以运⾏吗?如果要在⼀台没有操作系 统的机器上运⾏Hello World需要什么?应该怎么实现?
? printf是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在 终端上输出字符串?
? Hello World程序在运⾏时,它在内存中是什么样⼦的?

如果对这些问题有兴趣,如果你也是一个对问题刨根问底的人,请和我一起进一步探索学习程序的原理。

编译

      嵌入式程序开发过程中,功能简单的程序xx-gcc一条指令足够应付,稍微复杂的程序可能需要写一个Makefile。但是当我们开发大型项目是或者依赖较多是,遇到问题往往是不知所措,其程序的很多莫名其妙的错误让我们⽆所适从,⾯对程序运⾏时种种性能瓶颈我们束⼿⽆策。我们看到的是这些问题的现象,但是却很难看清本质,所有这些问题的本质就是软件运⾏背后的机理及⽀撑软件运⾏的各种平台和⼯具,如果能够深⼊了解这些机制,那么解决这些问题就能够游刃有余,收放⾃如了。

工具链

 所谓的工具链(toolchain),包含工具和链两部分含义:

工具:tool ,用来干活的家伙,这里要干的活:生成(可以运行的)程序或库文件而为了达成此目标,内部的执行过程和逻辑主要包含了:
编译:
        编译的输入(对象)是:程序代码

        编译输出(目标)是:目标文件

        编译所需要的工具是:编译器

        编译器,常见的编译器,即为gcc

链接:

        链接的输入(对象)是:(程序运行时所依赖的,或者某个库所依赖的另外一个)库(文件)

        链接的输出(目标)是:程序的可执行文件,或者是可以被别人调用的完整的库文件

        链接所需要的工具是:链接器

        链接器,即ld

         此处,为了将程序代码,编译成可执行文件,涉及到编译,链接(等其他步骤),要依赖到很多相关的工具,最核心的是编译器gcc,链接器ld。

:chain,之所以能称为链,你是说明不止一个东西,然后,按照对应的逻辑,串在一起,链在一起而对应的,涉及到的:
不止一个东西,指的是就是前面所说的那个工具,即:和程序编译链接等相关的gcc,binutils等工具
按照对应的逻辑,指的就是,按照程序本身编译链接的先后顺序,即:先编译,后链接,再进行后期其他的处理等等,比如用objcopy去操作相应的目标文件等等。
    如此的,将:和程序编译链接等相关的gcc,binutils等工具按照先编译后链接等相关的编译程序的内在逻辑串起来,就成了我们所说的:工具链

     下面我们简单看一下linux 一个应用程序的编译过程

gcc -v -o  test test.c #使用 -v 选型可以看到完整的编译过程

 /usr/lib/gcc/x86_64-linux-gnu/9/cc1 -quiet -v -imultiarch x86_64-linux-gnu test.c -quiet -dumpbase test.c -mtune=generic -march=x86-64 -auxbase test -version -fasynchronous-unwind-tables -fstack-protector-strong -Wformat -Wformat-security -fstack-clash-protection -fcf-protection -o /tmp/cchCtUAT.s 
 
 as -v --64 -o /tmp/ccr7QQVW.o /tmp/cchCtUAT.s
 
 /usr/lib/gcc/x86_64-linux-gnu/9/collect2 -plugin /usr/lib/gcc/x86_64-linux-gnu/9/liblto_plugin.so -plugin-opt=/usr/lib/gcc/x86_64-linux-gnu/9/lto-wrapper -plugin-opt=-fresolution=/tmp/ccOUOoYU.res -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s -plugin-opt=-pass-through=-lc -plugin-opt=-pass-through=-lgcc -plugin-opt=-pass-through=-lgcc_s --build-id --eh-frame-hdr -m elf_x86_64 --hash-style=gnu --as-needed -dynamic-linker /lib64/ld-linux-x86-64.so.2 -pie -z now -z relro -o test /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/Scrt1.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crti.o /usr/lib/gcc/x86_64-linux-gnu/9/crtbeginS.o -L/usr/lib/gcc/x86_64-linux-gnu/9 -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu -L/usr/lib/gcc/x86_64-linux-gnu/9/../../../../lib -L/lib/x86_64-linux-gnu -L/lib/../lib -L/usr/lib/x86_64-linux-gnu -L/usr/lib/../lib -L/usr/lib/gcc/x86_64-linux-gnu/9/../../.. /tmp/ccr7QQVW.o -lgcc --push-state --as-needed -lgcc_s --pop-state -lc -lgcc --push-state --as-needed -lgcc_s --pop-state /usr/lib/gcc/x86_64-linux-gnu/9/crtendS.o /usr/lib/gcc/x86_64-linux-gnu/9/../../../x86_64-linux-gnu/crtn.o


 根据gcc 输出可以看出来,对于一个简单C程序来说,从源码构建出C程序来讲,主要经过编译、汇编和链接三步,实际上编译又包含预编译和编译两步,所以可以说是四步:

预编译:

⾸先是源代码⽂件hello.c和相关的头⽂件,如stdio.h等被预编译器cpp预编 译成⼀个.i⽂件。对于C++程序来说,它的源代码⽂件的扩展名可能是.cpp 或.cxx,头⽂件的扩展名可能是.hpp,⽽预编译后的⽂件扩展名是.ii。第⼀ 步预编译的过程相当于如下命令(-E表⽰只进⾏预编译):

$gcc –E test.c –o test.i
预编译过程主要处理那些源代码⽂件中的以 “#” 开始的预编译指令。⽐如 “#include”、 “#define 等,主要处理规则如下:
? 将所有的 “#define 删除,并且展开所有的宏定义。
? 处理所有条件预编译指令,⽐如 “#if” “#ifdef” “#elif” “#else” “#endif
? 处理 “#include 预编译指令,将被包含的⽂件插⼊到该预编译指令的位 置。注意,这个过程是递归进⾏的,也就是说被包含的⽂件可能还包含其 他⽂件。
? 删除所有的注释 “//” “/* */”
? 添加⾏号和⽂件名标识,⽐如 #2“hello.c”2 ,以便于编译时编译器产⽣调 试⽤的⾏号信息及⽤于编译时产⽣编译错误或警告时能够显⽰⾏号。
? 保留所有的 #pragma 编译器指令,因为编译器须要使⽤它们。
经过预编译后的 .i ⽂件不包含任何宏定义,因为所有的宏已经被展开,并 且包含的⽂件也已经被插⼊到.i ⽂件中。所以当我们⽆法判断宏定义是否 正确或头⽂件包含是否正确时,可以查看预编译后的⽂件来确定问题。

       

编译:

使用cc1 进行编译产生汇编代码/tmp/cchCtUAT.s ,编译过程就是把预处理完的⽂件进⾏⼀系列词法分析、语法分析、语义分 析及优化后⽣产相应的汇编代码⽂件,这个过程往往是我们所说的整个程 序构建的核⼼部分,也是最复杂的部分之⼀。

$gcc –S test.i –o test.s

现在版本的 GCC 把预编译和编译两个步骤合并成⼀个步骤,使⽤⼀个叫做 cc1的程序来完成这两个步骤。

汇编:

使用as进行汇编,产生目标文件 /tmp/ccr7QQVW.o,汇编器是将汇编代码转变成机器可以执⾏的指令,每⼀个汇编语句⼏乎都 对应⼀条机器指令。所以汇编器的汇编过程相对于编译器来讲⽐较简单, 它没有复杂的语法,也没有语义,也不需要做指令优化,只是根据汇编指 令和机器指令的对照表⼀⼀翻译就可以了,“汇编这个名字也来源于此。 上⾯的汇编过程我们可以调⽤汇编器as来完成:

$as test.s –o test.o

或者:或者使⽤gcc命令从C源代码⽂件开始,经过预编译、编译和汇编直接输出 ⽬标⽂件(Object File):

$gcc –c test.c –o test.o

链接:使用collect2 (最终调用ld)进行链接,产生我们最终需要的可执行程序

目标文件

编译器编译源代码后⽣成的⽂件叫做⽬标⽂件,那么⽬标⽂件⾥⾯到底存放的是什么呢?或者我们的源代码在经过编译以后是怎么存储的?我们将 在这⼀节剥开⽬标⽂件的层层外壳,去探索它最本质的内容。

⽬标⽂件从结构上讲,它是已经编译后的可执⾏⽂件格式,只是还没有经过链接的过程,其中可能有些符号或有些地址还没有被调整。其实它本⾝ 就是按照可执⾏⽂件格式存储的,只是跟真正的可执⾏⽂件在结构上稍有不同。

可执⾏⽂件格式涵盖了程序的编译、链接、装载和执⾏的各个⽅⾯。了解它的结构并深⼊剖析它对于认识系统、了解背后的机理⼤有好处。

静态连接

动态连接

运行库

程序装载与执行

总结

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值