各位观众姥爷是否还记得在这一章的第一节,我们介绍过一个概念叫链接,静态链接就是链接的一种,不记得的滚回去温习❗❗PWN二进制安全修仙秘籍【第二章#二进制文件篇01】从源代码到可执行文件https://blog.****.net/weixin_53801131/article/details/142418934
那么这一节主要讲静态链接的过程
1. 示例代码编写
以下内容完全抄袭以下文章,可以移步至下述文章进行阅读????
静态链接与动态链接https://blog.****.net/weixin_50749380/article/details/134931262
首先我们先创建一个目录叫做 test ,test 目录作为根目录,根目录下创建四个源文件,分别是 add.c、sub.c、div.c 和 test.h。
目录图长这样????
这四个源文件内容分别如下:
test.h头文件定义三种运算,注意头文件只进行定义????
// test.h
#ifndef __TEST_H_
#define __TEST_H_
int add(int a,int b);
int sub(int a,int b);
int div(int a,int b);
#endif
add.c文件用于计算变量a和b的和????
// add.c
#include "test.h"
int add(int a,int b) {
return a + b;
}
sub.c文件用于计算a和b的差????
// sub.c
#include "test.h"
int sub(int a,int b) {
return a - b;
}
div.c文件用于计算a和b的商????
// div.c
#include "test.h"
int div(int a,int b) {
return a / b;
}
注意区别include后面接的是方括号还是双引号:
include" "是先从程序源文件所在目录查找,若未找到,则去系统默认目录查找。
而include<>直接去系统默认目录查找。
2. 编译示例代码
2.1 编译目标文件
将所有源文件都编译成目标文件:
gcc -c *.c
*.c
表示所有以.c
结尾的文件,也即所有的源文件。执行完该命令,会发现 test 目录中多了三个目标文件,分别是 add.o
、sub.o
和 div.o
。
2.2 打包成静态库文件
把所有目标文件打包成静态库文件:
ar rcs libtest.a *.o
*.o
表示所有以.o
结尾的文件,也即所有的目标文件。执行完该命令,发现 test 目录中多了一个静态库文件 libtest.a
,大功告成。
我们也可以通过以下命令查看静态链接库内的文件:
ar -t libtest.a
2.3 规范化目录及文件
在比较规范的项目目录中;lib
文件夹一般用来存放库文件;include
文件夹一般用来存放头文件;src
文件夹一般用来存放源文件;bin
文件夹一般用来存放可执行文件。
|-- include
| -- test.h
|-- lib
| -- libtest.a
|-- src
| -- main.c
main.c文件可以像下面这样使用 libtest.a
中的函数,main.c文件内容如下????
#include <stdio.h>
#include "test.h" //必须引入头文件
int main() {
int m, n;
printf("Input two numbers: ");
scanf("%d %d", &m, &n);
printf("%d+%d=%d\n", m, n, add(m, n));
printf("%d-%d=%d\n", m, n, sub(m, n));
printf("%d÷%d=%d\n", m, n, div(m, n));
return 0;
}
在编译 main.c
的时候,我们需要使用-I
(大写的字母i)选项指明头文件的包含路径;使用-L
选项指明静态库的包含路径;使用-l
(小写字母L)选项指明静态库的名字。所以,main.c
的完整编译命令为:
gcc src/main.c -I include/ -L lib/ -l test -o math
注意,使用
-l
选项指明静态库的名字时,既不需要lib
前缀,也不需要.a
后缀,只能写test
,GCC
会自动加上前缀和后缀。
运行编译生成后的文件
发现除号好像错了,不管了,接着往下看
3. 静态链接的过程
静态链接做的最重要的两件事
符号解析
和
重定位
先说符号解析是什么
符号解析就是将每个符号(符号就是函数、全局变量、静态变量这些能变的)的引用与其定义进行关联。
重定位又是什么咧?别急,我知道你很急,但你先别急
重定位就是将符号的定义与一个内存地址进行关联,然后修改这些符号的引用,让它指向这个内存地址。
3.1 分析静态链接过程的示例代码
下面再利用以下代码分析静态链接的过程
先是一个main.c文件,具体内容如下
//main.c
extern int shared;
extern void func(int *a, int *b);
int main() {
int a=100;
func(&a, &shared);
return 0;
}
然后是func.c文件,具体内容如下
//func.c
int shared = 1;
int tmp = 0;
void func(int *a, int *b) {
tmp = *a;
*a = *b;
*b= tmp;
}
使用以下命令对上述两个文件进行链接,形成一个可执行文件,并保留编译过程的中间文件。
gcc -static -fno-static-protector main.c func.c -save-temps --verbose -o func.ELF
下面这些都是我生成的文件
3.2 对示例代码的地址进行分析
上面这么多中间文件,我们这里只看中间产物main.o和静态链接可执行文件func.ELF
使用objdump命令查看文件各个节的详细信息
这里我们对比来看,着重看.text、.data和.bss节
> objdump -h main.o
main.o: file format pe-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000040 0000000000000000 0000000000000000 0000012c 2**4
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000000 0000000000000000 0000000000000000 00000000 2**4
ALLOC, LOAD, DATA
2 .bss 00000000 0000000000000000 0000000000000000 00000000 2**4
ALLOC
> objdump -h func.ELF
func.ELF: file format pei-x86-64
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00001d18 0000000000401000 0000000000401000 00000400 2**4
CONTENTS, ALLOC, LOAD, READONLY, CODE, DATA
1 .data 000000e0 0000000000403000 0000000000403000 00002200 2**4
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000980 0000000000407000 0000000000407000 00000000 2**5
ALLOC
解释一下,VMA(Virtual Memory Address)是虚拟地址
LMA(Local Memory Address)是加载地址
尚未进行链接的过程文件main.o的VMA都是0,而在链接完成后的func.ELF中,相似节被合并,且完成了虚拟地址的分配。
3.3 对中间文件main.o进行分析
使用objdump查看main.o的反汇编代码,参数“-mi386:intel”表示以intel格式输出
> objdump -d -M intel --section=.text main.o
main.o: file format pe-x86-64
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push rbp
1: 48 89 e5 mov rbp,rsp
4: 48 83 ec 30 sub rsp,0x30
8: e8 00 00 00 00 call d <main+0xd>
d: c7 45 fc 64 00 00 00 mov DWORD PTR [rbp-0x4],0x64
14: 48 8d 45 fc lea rax,[rbp-0x4]
18: 48 8b 15 00 00 00 00 mov rdx,QWORD PTR [rip+0x0] # 1f <main+0x1f>
1f: 48 89 c1 mov rcx,rax
22: e8 00 00 00 00 call 27 <main+0x27>
27: b8 00 00 00 00 mov eax,0x0
2c: 48 83 c4 30 add rsp,0x30
30: 5d pop rbp
31: c3 ret
在地址0x22处,可以看到main函数的地址从0开始,对func函数的调用在偏移0x27处。
0xe8是call指令的操作码,后四个字节是被调用函数相对于调用指令的下一条指令的偏移量;此时符号还没用重定位,相对偏移为0x00000000,通过计算call指令调用的地址为0x20+(-0)=0x20,要注意这只是一个临时地址。
3.4 对静态链接可执行文件func.ELF进行分析
同理,进行分析
>objdump -d -M intel --section=.text func.ELF | grep -A 16 "<main>"
0000000000401550 <main>:
401550: 55 push rbp
401551: 48 89 e5 mov rbp,rsp
401554: 48 83 ec 30 sub rsp,0x30
401558: e8 23 01 00 00 call 401680 <__main>
40155d: c7 45 fc 64 00 00 00 mov DWORD PTR [rbp-0x4],0x64
401564: 48 8d 45 fc lea rax,[rbp-0x4]
401568: 48 8b 15 21 2f 00 00 mov rdx,QWORD PTR [rip+0x2f21] # 404490 <.refptr.shared>
40156f: 48 89 c1 mov rcx,rax
401572: e8 19 00 00 00 call 401590 <func>
401577: b8 00 00 00 00 mov eax,0x0
40157c: 48 83 c4 30 add rsp,0x30
401580: 5d pop rbp
➜ hehao objdump -d -M intel --section=.text func.ELF | grep -A 16 "<func>"
0000000000401590 <func>:
401590: 55 push rbp
401591: 48 89 e5 mov rbp,rsp
401594: 48 89 4d 10 mov QWORD PTR [rbp+0x10],rcx
401598: 48 89 55 18 mov QWORD PTR [rbp+0x18],rdx
40159c: 48 8b 45 10 mov rax,QWORD PTR [rbp+0x10]
在地址0x401572处,调用func函数的call指令,其下一条mov指令位置0x401577,因此相对于mov指令偏移量为0x19的地址为0x401577+0x19=0x0x401590
3.5 分析重定位表
重定位表的作用是什么?
它是可重定位文件中最重要的内容,用于告诉链接器如何修改节的内容。每个重定位表对应一个需要被重定位的节,如下所示
> objdump -r main.o
main.o: file format pe-x86-64
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
0000000000000009 R_X86_64_PC32 __main
000000000000001b R_X86_64_PC32 .refptr.shared
0000000000000023 R_X86_64_PC32 func
有三个偏移量,说明.text存在三个重定位入口。类型为R_X86_64_PC32说明为相对寻址(CPU将指令中编码的32位值加上PC下一条指令地址的值得到有效地址),还有一种是R_X86_64_32用于绝对寻址(直接使用在指令中编码的32位值作为有效地址)。