2017-2018-1 20155205 《信息安全系统设计基础》第十三周学习总结
一、带着问题学习第七章《链接》
『问题一』:编译驱动程序如何工作?
- 根据教材的指导,新建两个c文件main.c和swap.c:
void swap();
int buf[2] = {1, 2};
int main()
{
swap();
return 0;
}
void swap();
extern int buf[];
int *bufp0 = &buf[0];
int *bufp1;
void swap()
{
int temp;
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
编译驱动程序所完成的工作,如下图:
先是由预处理器(cpp)将main.c翻译成中间文件:main.i,接下来是编译器(cc1)将main.i翻译成汇编文件main.s。然后是汇编器(as)将main.s翻译成一个可重定位的目标文件main.o。最后由链接器(ld)将main.o和swap.o以及一些系统目标文件组合起来,创建可执行目标文件。
- 在以上的这个过程中ld链接器的主要工作:
① 符号解析:目标文件定义和引用符号,符号解析的目的是将每个符号引用和一个符号定义联系起来;
②重定位:把每个符号定义与一个存储器位置联系起来,然后修改对这些符号的引用,是的他们指向这个存储器位置,从而实现重定位。
『问题二』:链接器操作的目标文件究竟是什么?
1、可重定位目标文件
-
目标文件有三种形式:
1、可重定位目标文件。包含二进制代码和数据,其形式可以在编译时与其他可重定位目标文件合并起来,创建一个可执行目标文件。
2、可执行目标文件。包含二进制代码和数据,其形式可以被直接拷贝到存储器并执行。
3、共享目标文件。一种特殊类型的可重定位目标文件,可以在加载或者运行地被动态地加载到存储器并链接。
编译器和汇编器生成可重定位目标文件(包括共享目标文件)。链接器生成可执行目标文件。从技术上来说,一个目标模块(object module)就是一个字节序列,而一个目标文件(object file)就是一个存放在磁盘文件中的目标模块。
- 下图说明了一个目标文件生成可执行文件,然后加载到内存后的映射等,三个步骤:
- 教材里重点学习了典型的ELF可重定位目标文件的格式,也就是上图中最左面的图,如下图所示:
解释:
.text:已编译程序的机器代码
.rodada:只读数据,比如printf语句中的格式串。
.data:已经初始化的全局C变量。局部变量在运行时保存在栈中。即不再data节也不在bss节
.bss:未初始化的全局C变量。不占据实际的空间,仅仅是一个占位符。所以未初始化变量不需要占据任何实际的磁盘空间。C++弱化BSS段。可能是没有,也可能有。
.symtab:一个符号表,它存放“在程序中定义和引用的函数和全局变量的信息”。
.rel.text:一个.text节中位置的列表。(将来重定位使用的)
.rel.data:被模块引用或定义的任何全局变量的重定位信息。
.debug:调试符号表,其内容是程序中定义的局部变量和类型定义。
.line:原始C源程序的行号和.text节中机器指令之间的映射。
.strtab:一个字符串表.
2、符号和符号表
- 保存于.symtab中的是一个符号表,其是定义和引用函数和全局变量的信息。有三种不同类型的符号:全局符号(不带static),外部引用(external)和本地符号。如果是带有static符号的就会在.data和.bss中为每个定义分配空间,并在.symtab中创建一个唯一名字的本地符号。
typedef struct {
int name; /*String table offset*/
int value; /*Section offset, or VM address*/
int size; /*Object size in bytes*/
char type:4, /*Data, fund,section,or src file name (4 bits)*/
binding:4; /* Local of global(4bits)*/
char reserved; /*Unused*/
char section; /*Section header index ABS UNDEF*/
}Elf_Symbol;
『问题三』:链接器工作的过程是什么?
① 符号解析
-
符号解析任务简单的说,就是链接器使得所有模块中的每个符号只有一个定义。链接器在这一个阶段的主要任务就是把代码中的每个符号引用和确定的一个符号定义联系起来。对于本地符号,这个任务相对来说是简单的。复杂的就是全局符号,编译器(cc1)遇到不是在当前模块中定义的符号时,会假设该符号的定义在其他模块中,生成一个链接器符号交给链接器处理。如果链接器ld在所有的模块中都找不到定义的话就会抛出异常。
-
链接器使用下面的规则来处理多重定义的符号:
规则1:不允许多个强符号;
规则2:如果有一个强符号和多个弱符号,那么选择强符号;
规则3:如果有多个弱符号,那么这些弱符号中任意选择一个;
② 重定位
- 重定位算法
foreach section s{
foreach relocation entry r{
refptr = s + r.offset/* ptr to reference to be relocated */
/* Relocate a PC-relative reference */
if (r.type == R_386_PC32){
refaddr = ADDR(s) + r.offset; /* ref's run-time address */
*refptr = (unsigned)(ADDR(r.symbol) + *refptr - refaddr);
}
/* Relocate an absolute reference */
if (r.type == R_386_32)
*refptr = (unsigned)(ADDR(r.symbol) + *refptr)
}
}
- 怎么看重定位符号表呢?我们知道
gobjdump -d xxx.o
(mac os 用法,linux中为objdump -d xxx.o
)可以查看反汇编代码,那么我们查看一下objdump的帮助文档,发现-r
选项和重定位(reloc)有关。
输入gobjdump -r main.o
看到了重定位符号表:
『问题四』:链接器完成工作后生成的目标文件是什么?
- 下图是可执行文件的格式:
解释:
ELF头部:描述文件总体格式,标注出程序入口点;
.init:定义了初始化函数;
段头部表:可执行文件是一个连续的片,段头部表中描述了这种映射关系;
- 根据之前创建的main.c和swap.c,可以得到生成的执行文件的反汇编代码:
-
解释:在段头部表中,我们会看到程序初始化为两个存储器字段,行1和行2是代码段,有读和执行的权限(flags:r-x),开始于存储器地址0x08048000处(vaddr/paddr),该字段大小为0x448(memsz),并且初始化为可执行目标文件的头0x448个字节(filesz);行3和行4是数据段,有读写的权限(flags),开始于存储器地址:0x08049448处,总大小0x104个字节(memsz),从文件偏移0x448(off)处开始的0xe8(filesz)个字节初始化。
-
下图是运行时的存储器映像:
什么是加载?
说白了就是将程序拷贝到存储器并运行的过程。这里是由execev函数来调用加载器(驻留在存储器中)完成的,我们要执行p文件的时候,就是使用./p来,加载器就把p的数据和代码拷贝从磁盘拷贝到了存储器中,并通过跳转到ELF头部中的程序入口点开始程序p的执行。
怎样加载?
当加载器运行时,就先创建一个存储器映像(上图所示),在ELF可执行文件头部表的指示下,加载器将可执行文件的代码和数据段拷贝到0x0804800处向上的两个段中,然后跳转到程序入口点_start(在ctrl.o中定义)开始执行。
『问题五』:如何动态链接共享库?
- Linux为动态链接器提供了一系列简单的接口:
#include <dlfcn.h>
void *dlopen(const char *filename, int flag);//加载共享库
void *dlsym(void *handle, char *symbol); //指向一个共享库的句柄和一个符号名字。
int dlclose(void *handle); //下载共享库
const char *dlerror(void); //容错
- 下面是一个动态加载和链接共享库的应用程序:
#include <stdio.h>
#include <dlfcn.h>
int x[2] = {1, 2};
int y[2] = {3, 4};
int z[2];
int main()
{
void *handle;
void (*addvec)(int *, int *, int *, int);
char *error;
/* dynamically load the shared library that contains addvec() */
handle = dlopen("./libvector.so", RTLD_LAZY);
if (!handle) {
fprintf(stderr, "%s
", dlerror());
exit(1);
}
/* get a pointer to the addvec() function we just loaded */
addvec = dlsym(handle, "addvec");
if ((error = dlerror()) != NULL) {
fprintf(stderr, "%s
", error);
exit(1);
}
/* Now we can call addvec() it just like any other function */
addvec(x, y, z, 2);
printf("z = [%d %d]
", z[0], z[1]);
/* unload the shared library */
if (dlclose(handle) < 0) {
fprintf(stderr, "%s
", dlerror());
exit(1);
}
return 0;
}
说明:
1>使用dlopen打开本地libvector.so共享库,并解析库中的符号;
2>使用dlsym访问其中的addvec函数,如果存在就返回该函数的地址;
3>使用dlclose卸载共享库;
二、与编译驱动相关的gcc基础学习
① GCC编译选项解析
常用编译选项
-
命令格式:gcc [选项] [文件名]
-E:仅执行编译预处理;
-S:将C代码转换为汇编代码;
-c:仅执行编译操作,不进行连接操作;
-o:指定生成的输出文件。
gcc编译的四个阶段(ESc,iso)
1.将hello.c预处理输出hello.i文件。
gcc -E hello.c -o hello.i
查看hello.i内容如下:
2.将预处理输出文件hello.i汇编成hello.s文件。
gcc -S hello.i -o hello.s
查看hello.s内容如下:
3.将汇编输出文件hello.s编译输出hello.o文件。
gcc -c hello.s -o hello.o
查看hello.o内容如下:
4.将编译输出文件hello.o链接成最终可执行文件hello。
gcc hello.o -o hello
- 或直接输入下面代码一步到位
gcc hello.c -o hello
- 当有多个文件需一起编译时,可输入
gcc *.c
一起编译。
② 多模块
多个文件一起编译
文件:test_a.c test_b.c
两种编译方法:
1、一起编译
gcc test_a.c test_b.c -o test
2、分别编译各个源文件,之后对编译后输出的目标文件链接
gcc -c test_a.c //生成test_a.o
gcc -c test_b.c //生成test_b.o
gcc -o test_a.o test_b.o -o test
比较:第一中方法编译时需要所有文件重新编译;第二种植重新编译修改的文件,未修改的不用重新编译。
『一个实例:-I参数的使用』
- 使用vim编辑生成相应代码,注意:*.h文件应放入include中, *.c文件应放入src中。
- 编译时若输入
gcc *.c
会出现下图错误
- 应输入
gcc -I/头文件所在路径 *.c
才能够成功编译
这里我们为之后做题方便,可以输入gcc -I/头文件所在路径 *.c -o main
将执行文件命名为main。
- -I参数可以用相对路径,比如头文件在当前目录,可以用-I.来指定。
③ 静态库与动态库
静态函数库
这类库的名字一般是libxxx.a;利用静态函数库编译成的文件比较大,因为整个 函数库的所有数据都会被整合进目标代码中,他的优点就显而易见了,即编译后的执行程序不需要外部的函数库支持,因为所有使用的函数都已经被编译进去了。当然这也会成为他的缺点,因为如果静态函数库改变了,那么你的程序必须重新编译。
动态函数库
这类库的名字一般是libxxx.so;相对于静态函数库,动态函数库在编译的时候 并没有被编译进目标代码中,你的程序执行到相关函数时才调用该函数库里的相应函数,因此动态函数库所产生的可执行文件比较小。由于函数库没有被整合进你的程序,而是程序运行时动态的申请并调用,所以程序的运行环境中必须提供相应的库。动态函数库的改变并不影响你的程序,所以动态函数库的升级比较方便。
『一个实例』
- 在上一个实例中有五个.c文件,除了main.c外,其他4个模块(add.c sub.c mul.c div.c)的源代码不想给别人,如何制作成一个静态库libmath.a和一个共享库libmath.so,制作完成后如何调用?
我们先来看看静态库libmath.a的制作:
1.输入gcc -c -I/头文件所在路径 add.c sub.c mul.c div.c
生成四个.o文件。
2.输入ar rcvs libmath.a add.o sub.o mul.o div.o
生成静态库。
3.如果之前已经生成了main,我们可以输入rm main
来删掉它,ls一下可以看到没有执行文件了。
再输入gcc main.c -o main -I/头文件所在路径 -L. -lmath
,可以看到生成了执行文件main,输入./main
就可以实现调用静态库来执行文件了!
- 这里要注意,gcc会在静态库名前加上前缀lib,然后追加扩展名.a得到的静态库文件名来查找静态库文件。那么为什么要在math之前加-l呢?这是因为-l参数就是用来指定程序要链接的库,-l参数紧接着就是库名。而-L参数跟着的是库文件所在的目录名。题中的库文件在当前目录,故写为
-L.
。
我们再来看看动态库libmath.so的制作:
1.输入gcc -fPIC -c -I/头文件所在路径 add.c sub.c mul.c div.c
2.输入gcc -shared -o libmath.so add.o sub.o mul.o div.o
生成动态库。
3.我们来输入gcc -I/头文件所在路径 -o main main.c -L. -lmath
后执行一下main试一试,会发现出现下图的错误。
这是为什么呢?原来是找不到动态库文件libmath.so。程序在运行时,会在/usr/lib和/lib等目录中查找需要的动态库文件。若找到,则载入动态库,否则将提示类似上述错误而终止程序运行。我们将文件libmath.so复制到目录/usr/lib中,再试试就可以执行啦!
三、习题学习
『习题7.6』
- 对于下面的swap.c函数进行分析:
extern int buf[];
int *bufp0 = &buf[0];
static int *bufp1;
static void incr()
{
static int count = 0;
count++;
}
void swap()
{
int temp;
incr();
bufp1 = &buf[1];
temp = *bufp0;
*bufp0 = *bufp1;
*bufp1 = temp;
}
- 答案:
符号 | swap.o.symtab条目 | 符号类型 | 定义符号的模块 | 所属的节 |
---|---|---|---|---|
buf | y | extern | main.o | .data |
bufp0 | y | global | swap.o | .data |
bufp1 | y | local | swap.o | .bss |
swap | y | global | swap.o | .text |
temp | n | - | - | - |
incr | y | local | swap.o | .text |
count | y | local | swap.o | .data |
- 分析:要掌握全局变量和函数是属于symtab的,其次要掌握可重定位目标文件的格式各项的内容,结合家庭作业7.1便可得出结果。在我做题的过程中,对符号类型出现了错误判断,比如:bufp1在7.1中是全局符号,但在7.6中加上了static属性的限制,因此变成了本地符号。
『习题7.7』
- 答案:在double x前加上static,使之变成本地强符号。
『习题7.8』
- 答案:
A.
a)REF(main.1)-->DEF(main.1)
b)REF(main.2)-->DEF(main.2)
B. UNKNOWN
C. ERROR
-分析:A中和家庭作业7.2不同的是给main加上了static,因此在module2中的main是本地的;B中出现了两个弱符号,因此随便选择;C中出现了两个强符号,因此错误。
『习题7.9』
- 答案:p2中的main是弱符号,链接后的引用是foo6.c中的函数main。 main的第一个字节就是0x55(pushl %ebp),所以能打印出0x55。
『习题7.10』
- 答案:
A. gcc p.o libx.a p.o
B. gcc p.o libx.a liby.a libx.a
C. gcc p.o liby.a libx.a liby.a libz.a
『习题7.12』
- 答案
行号 | 地址 | 值 |
---|---|---|
15(bufp0) | 0x080483CB | 0x0804945C |
16(buf[1]) | 0x080483D0 | 0x08049458 |
18(bufp1) | 0x080483D8 | 0x08049548 |
18(buf[1]) | 0x080483DC | 0x08049458 |
23(bufp1) | 0x080483E7 | 0x08049548 |
四、反馈
- 同伴(20155218 徐志瀚)对我的反馈:我的同伴郝博雅同学在这篇博客里,带着问题重新学习了第七章,坚持问题导向,学习到了新的知识点,解决的自己的疑惑。在第七章的学习中,共分为了三个问题,每个问题的解答都是很详细的,既有基础知识,也有直截了当的图片和代码。在小内容的学习中,也是不断地通过提出问题来引导下一步的学习,让我非常直接深刻的认识到同伴为什么学习是为了哪方面的知识,需要哪方面的知识。解答过程中,步骤也是很详细,每一步都有操作的方法和结果截图,让我也可以按照步骤自己操作实现。在看同伴的博客时,发现她的博客形式非常的简洁明了,相比之下,自愧不如。