参考材料列表:
1.如何使用gcc编译器? http://linux.chinaunix.net/doc/2004-10-05/22.shtml 点击打开链接
2.gcc各工具使用简介 http://blog.sina.com.cn/s/blog_6b94d5680101p7fm.html 点击打开链接
3.linux程序调试——查看二进制文件 http://blog.sina.com.cn/s/blog_7a2fc53a0100y54h.html 点击打开链接
4.查看.o, .obj文件符号列表,强大的nm命令 http://blog.csdn.net/pendle/article/details/5942887 点击打开链接
5.Understanding ELF using readelf and objdump http://blog.csdn.net/wbd880419/article/details/6794592
**********************************************************************************
一《理解预编译阶段》
首先,建立程序材料:
在你选择的目录下建立link_header.h程序如下
#ifndef DECK_H
#define DECK_H
#define DECKSIZE 52
typedef struct deck_t
{
int card[DECKSIZE];
int dealt;
}deck_t;
#endif
再建立link.c程序如下
#include <stdio.h>
#include "link_header.h"
int main()
{
printf("Hello world!\n");
}
然后我们用gcc工具对源文件进行预编译:gcc -E -o link.txt link.c。可以得到预编译后的文件link.txt(一般是以".i"为默认后缀,不过linux对后缀没有什么要求倒是。但是这里不太明白,是说如果我们直接gcc的话,将把预编译结果放在.i文件中吗?)
——》对预编译后的文件进行观察,发现将stdio.h(先)和link_header.h(后)文件中的内容“按顺序”插入在link.c的文件之前,还加入了一些#开头的行。
是不是“按在源文件中include的顺序进行包含的呢?”进行实验,将2个include的顺序调换,变成
#include "link_header.h"
#include <stdio.h>
再次gcc -E -o link.txt link.c。然后查看link.txt。发现果然插入顺序变成了link_header.h(先)stdio.h(后)。说明include顺序影响了插入顺序
——》对预编译后的文件进行观察,发现插入头文件的内容完全没有改变没有什么优化之类的。
但是,也不完全没有优化,做实验:我在link_header.h的中加入了注释:// haha just for check
这一句在预编译后的.txt中不会存在
——》再观察,观察3个文件的大小,由link.c和link_header.h生成了link.txt
用ls -lh观察3个文件的大小,得到以下结果:
*看到txt文件为17KB,并不等于.c的87B + .h的149B
哦,还include的了stdio.h。于是通过whereis stdio.h找到了stdio.h的位置,再用ls -lh查看大小
*holy shit are you serious?一个stdio.h的大小竟然比整个link.txt的还大。不过通过查看stdio.h看到,这个头文件中有较多的注释哦,这些是不会插入预编译后的文件中的,因此txt文件的大小不是包含了全部的.h大小。
*这就引出了值得思考的问题,我暂时的思考结果:一个文件A如果是a的大小,文件B(大小为b) include的了文件A,预编译后的txt文件大小与a+b是没有一定关系的:
(1)如上所述,如果一个.h文件中包含了较大量的注释,这些注释也是需要存储空间的,因此,这部分注释的存储空间不会增大.c的存储空间(也即,A包含了B,但是A预编译后的txt的存储空间可能比A+B小)
(2)文件A用include "B"包含了B,但是此时这条include语句对A的影响也只是include这一条语句所占的微乎其微的存储空间增加;
直到对文件A进行预编译后,此时才真正将B插入到A,这时B对A的存储空间影响量就是B中除去注释的其他语句的存储空间量。
——》注意如果没有在.h文件中加上#ifndef(header guard)的话,如果重复包含头文件,将会多次重复插入头文件里的内容,没有删减
**************************************************************************************
二《理解编译和汇编》——仔细理解《深入理解计算机系统》7.5
使用工具:
gcc -c
objdump -s 查看二进制文件各section的内容(-d选项将“代码段.text反汇编”)
readelf -s查看二进制文件的.symtab符号表(不查看内容)
nm 查看二进制文件中的各符号
查看文件类型 file
ctrl+z进入后台的程序,怎么重新到前台来?
先用1。jobs“显示停止的编辑文件” 2。fg+编号就可以了(打开正在编辑的文件) |
要做到:
1.对于一个源文件中出现的所有标识符,清楚的回答以下几个问题:该符号是否会出现在对应模块(.o文件)的.symtab中?符号类型是什么(本地,全局,外部)(不懂这个含义)?在哪个模块中定义?在模块中占据哪个section(.text, .data, .bss)?回答的结果可以用readelf -s来对照。例如对如下源文件
/* swap.c */
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
************************************************************
三.《理解链接》——仔细理解《深入理解计算机系统》7.6
——》首先,链接器的工作(解析符号引用)分为2类:解析在本模块中定义的引用;解析在其他模块中定义的引用
1.解析在本模块中定义的引用(见7.6第一段)
2.解析在其他模块中定义的引用。
对于如下源文件
int haha();
int f()
{
static int x = 0;
haha();
return x;
}
int g()
{
static int x = 1;
return x;
}
编译之后将得到以下结果(readelf -s)
红色标注的地方验证了书上说的“当编译器遇到一个不是在‘当前’模块中‘定义’的符号时,它会假设该符号是在其他摸个模块定义的”,于是在可重定位目标文件中,就有UNDEF
进一步:
如果没有
int haha();
——》然后,一个基本概念是,解析第一类符号(在本模块中定义)容易,解析第二类符号(定义在其他模块)难。
原因是:多个目标文件可能会定义多个相同的符号。而链接器对这种情况可能有多种处理方法。
“Unix系统采用的方法设计编译器,汇编器,链接器之间的协作,处理方法设计有强弱符号之分”。详见《深入理解计算机系统》7.6.1
——》上面是将几个源文件分别编译+将几个可重定位目标文件链接。这里是某个可重定位目标文件与静态库链接
1.为什么不按上面的方法分别编译+链接呢?这就是静态库出现的原因(7.6.2)
2.与静态库链接的步骤:分别编译+创建静态库(ar工具);在源文件中,包含静态库中要使用的例程的函数原型+用-static参数指示是静态链接+链接当前可重定位目标文件与静态库
3.“链接器”(在静态库中)将只选择拷贝某些模块,这些模块定义了当前可重定位目标文件引用到的符号。
*问题,是不是使用静态库中的函数,就一定要静态链接呢?
——》链接器使用静态库来解析引用de内部运行原理7.6.3
1.链接器维持的3个集合E,U,D
2.这种链接器处理算法,会在有时导致一些链接错误,因为命令行上的库和目标文件的顺序非常重要。(原因在于当一个静态库存档文件使用完后会被丢弃。因此当时没用到的模块如果后面再想用到时,则已经找不到了)
(思考的解决方法,可不可以将输入文件分2组,库文件一组,目标文件一组。先扫描所有的目标文件建立起完整的U,D,再使用库文件。p.s应该这也不完整,因为库文件的模块中也可能引用未在本模块定义的符号)
******************************************
7.7 重定位(对这一步完成的工作,和原理都不是很清楚)
但有一点是要有概念的,在重定位这个阶段,就会把各个输入模块中相同的section复制合并起来
******************************************
7.8 可执行文件
工具:查看可执行文件的segment header table:objdump(?如何做到7.8图7-12的样子?)
1.可执行目标文件包含要加载一个程序到存储器需要的“所有”信息(那么加载器如何受这些信息指导呢?见2)
2.段头部表(segment header table包含了加载到虚拟存储器的哪个地方的信息)
3.从图7.11看出,可执行目标文件也分section(如.text,.data等),但是指导如何加载的,是按照段头部表来的,段头部表将各个section按照运行时权限分为更少量的segment
******************************************
7.9 加载可执行目标文件
1.加载器可以由用户主动运行吗?(可以,程序调用execve函数来调用加载器)
2.加载器如何加载一个程序的。
——》每个Unix程序都有一个运行时存储器映像(run-time memory image)。要知道这个存储器映像的结构分布
——》加载器运行时,它给一个程序创建一个run-time memory image。然后根据可执行文件中的segment header table的指导,将可执行文件的对应segment“拷备“到存储器映像的对应位置
更准确一些,上一步确实可以直接拷贝,但是往往不这样做,只要最终达到可执行文件中某一段到存储器映像的某一段一一对应即可。真实做法是,加载器根据可执行文件中的segment header table的指导,将可执行文件的对应segment与存储器映像对应位置建立起”映射“,直到执行到某个地方,发现真正存储器中没有这些数据,然后才发生真实的从disk到memory的拷贝。
——》建立起映射后。开始执行,执行的步骤如7.9,概括的说就是先是执行一些函数,然后才是main函数。main函数返回之后,再是_exit使得”执行流“从本函数中——操作系统。(执行的这些步骤体现在图7-14中)
******************************************
7.10 动态链接共享库
以下几个概念需要有:
1.动态库是致力于解决静态库缺陷的现代产物(主要是多个可执行文件包含多个标准函数代码)
2.动态库的链接在两个地方不同,
一是在重定位时不会拷贝代码和数据section到可执行文件中,只拷贝一些重定位和符号表信息;
二是在”运行时“由“加载器”在可执行文件的‘.interp’ section中找到“动态链接器”,
先执行这个动态链接器完成剩余的链接工作:映射动态库的文本和数据到some memory segment(即真实内存的某个地方)—》将“上面部分链接的可执行文件中对动态库中符号的引用进行重定位”—》至此才完全完成所有链接工作。
然后这时应用程序才开始工作。
(即动态链接的执行流转移为:翻译器(预编译,编译,汇编)—》链接器(部分链接)—》加载器—》动态链接器—》应用程序运行。这也就是《深入计算机系统》图7-15要表达的意思)
******************************************
7.11 从应用程序中加载和链接共享库(没看)
从上面7.10的最后一段可以看出,和静态链接相比,动态链接直到应用函数执行前一刻“才”完全链接好;
但是和这一节相比,动态链接到应用函数执行前“已经”完全链接好了。这一节讲的是在运行时程序时如何要求动态链接器加载和链接“任意共享库”的。