.

链接方式:相似段合并,主要分为两步:

链接方式: ld a.o b.o -e main -o ab

不知道为什么会一直报这个错 a.c:(.text+0x4f): undefined reference to `__stack_chk_fail’

解决方法,在编译时加上 -fno-stack-protector标志,注意不是在链接时加

1
2
3
4
5
6
7
8
9
ttj@ttj ~/copy [1]> gcc -c a.c -fno-stack-protector -o a.o
a.c: In function ‘main’:
a.c:4:1: warning: implicit declaration of function ‘swap’ [-Wimplicit-function-declaration]
4 | swap(&a, &shared);
| ^~~~
ttj@ttj ~/copy> gcc -c b.c -fno-stack-protector -o b.o
ttj@ttj ~/copy> ld a.o b.o -e main -o ab
ttj@ttj ~/copy> ./ab
fish: './ab' terminated by signal SIGSEGV (Address boundary error)

目前来说,链接时设定的虚拟地址就是将来程序被加载进内存的地址,当然,内存的也是虚拟地址,不同进程之间的相同的虚拟地址可能被映射到不同的物理地址。

32 位 linux 下elf 虚拟地址分配从 0x08048000 开始,64位下从 0x400000 开始。

加载地址 与 虚拟地址 区别?一般是一样,对嵌入式系统可能不同

符号解析重定位

重定位

完成空间与地址分配后,进入了符号解析与重定位过程,这是静态链接的核心。

编译生成可重定位文件之后,会生成两类需要修正的汇编地址调用指令:绝对地址指令和相对地址指令,在链接之前,这些地址都只是一个临时的假地址,链接器在重定位阶段才会修正这些地址为真正的虚拟地址。

重定位表

通过什么判断那些地址需要被修正:重定位段(表),比如说.rel.text, .rel.data(实际上不是根据名字来判断是不是重定位段的,是根据段的某些属性来区分的)

重定位表的每个元素代表一个需要重定位的符号,他包含两个值:第一个是地址,对于可重定位文件是相对于段的偏移,对于可执行文件可共享目标文件是虚拟地址(很多地址都是像这样的,比如说符号表的符号地址,可重定位文件也是偏移,可执行文件也是虚拟地址),第二个是重定位入口的类型和符号,注意高24位给出了表在符号表的下标哦。

符号解析

对于在重定位位表中有的符号,符号表中该符号的索引会显示UND,如果链接完所有文件后,有些未定义符号不能再全局符号表中找到,则报错。

指令修正方式

对绝对地址而言,其原来保存的值(A)一般为 0,符号的实际地址为 S,相加后也是符号的实际地址

对相对寻址而言,其原来保存的值(A):32位处理器是 -4(这个可能和指令流水线有关吧,一般RISC-V架构,五级流水,当前指令执行时,读取的地址是后面第二条指令的地址)。原图说的这么复杂,真绕,可以先看下下面的解释。因为执行当前指令时,读取的地址其实已经是当前指令的下一地址,所以只要计算符号相对于下一地址的偏移,再加上下一地址(执行阶段取值得到的地址),就会得到我们要访问的符号的虚拟地址了。关键就在于如何获得符号相对于下一地址的偏移,其实用两个地址相减就行了。假设符号的虚拟地址为符号的虚拟地址,假设下一地址为P+4(其实就是P-A嘛),相减得S-P-4(不就是S-P+A吗)。

tips. from gpt

在 x86_64 架构中,指令是按顺序取指的,并且通常是顺序执行。因此,在当前指令执行时,指令取址(IF 阶段)会指向即将执行的下一条指令。相对于当前执行的指令地址来说:

顺序执行时,取指地址偏移量为当前指令的地址,在 x86_64 中,指令长度可变,通常在 1 到 15 字节之间。

目前本电脑上计算方式有些不同,(二刷没看懂呜呜呜)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
//a.o
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 55 push %rbp
5: 48 89 e5 mov %rsp,%rbp
8: 48 83 ec 10 sub $0x10,%rsp
c: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
13: 48 8d 45 fc lea -0x4(%rbp),%rax
17: 48 8d 35 00 00 00 00 lea 0x0(%rip),%rsi //相对寻址,但没有相对偏移地址
1e: 48 89 c7 mov %rax,%rdi
21: b8 00 00 00 00 mov $0x0,%eax
26: e8 00 00 00 00 callq 2b <main+0x2b>//相对寻址,但没有相对偏移地址
2b: b8 00 00 00 00 mov $0x0,%eax
30: c9 leaveq
31: c3 retq
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
//ab
0000000000401000 <main>:
401000: f3 0f 1e fa endbr64
401004: 55 push %rbp
401005: 48 89 e5 mov %rsp,%rbp
401008: 48 83 ec 10 sub $0x10,%rsp
40100c: c7 45 fc 64 00 00 00 movl $0x64,-0x4(%rbp)
401013: 48 8d 45 fc lea -0x4(%rbp),%rax
401017: 48 8d 35 e2 2f 00 00 lea 0x2fe2(%rip),%rsi # 404000 <shared> // 0x2fe2 + 0x40101e = 0x404000 刚好是 data 段开始位置
40101e: 48 89 c7 mov %rax,%rdi
401021: b8 00 00 00 00 mov $0x0,%eax
401026: e8 07 00 00 00 callq 401032 <swap>//0x2b + 0x07 = 0x32
40102b: b8 00 00 00 00 mov $0x0,%eax
401030: c9 leaveq
401031: c3 retq

0000000000401032 <swap>:
401032: f3 0f 1e fa endbr64

common 块

tips.链接器不知道变量的类型,对于每个符号,他只能通过符号表判断大致的类型,比如SECTION、OBJECT、FUNC。这也导致了链接器没法判断不同文件中的变量类型是否一致,不过从GCC 10 开始,未初始化的全局变量都放到.bss段而不是common块。既然不是放到common块中,那么变量就被当做强符号看待。如下所示,按照一般理论,一个强符号一个弱符号不会报错,但这里两者的符号都存放在.bss段,被视为强符号。

C++问题

程序员的自我修养.pdf

重复代码消除

比如模板、外部内联函数和虚函数表可能在不同的编译单元生成相同的代码,比如模板,一个文件使用了vector<int>,他不知道其他文件有没有使用,就只有把所有的的代码都保留下来,这会导致:

  • 空间浪费
  • 指令运行效率低,不容易被Cache命中
  • 地址容易出错

解决方法就是把每个模板的实例代码都放到一个段里,比如.temp.add<int>,这样链接器就可以区分相同的实例段并处理。外部内联函数和虚函数表也是类似。

全局构造和析构

C++&ABI

经常分不清ABI、API,这里讲清楚了

好长,其他的看原文吧,p140 OneDrive

静态库链接

尝试手动实现静态链接:

调用库太多,难以一个个添加完。

链接控制过程

链接器一般有如下三种方法:

  1. 使用命令行给链接器指定参数。如 -e -o
  2. 将链接指令存放到目标文件中,较常见,visual c++ 将连接参数放在 pe 目标文件的 .drectve段来传递参数。
  3. 使用连接控制脚本,最灵活,最强大。

可通过 ld -verbose查看默认链接脚本

可通过 ld -T link.script指定链接脚本

文件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
char* str = "Hello world!\n";
void print()
{
asm( "movl $13,%%edx \n\t"
"movl %0,%%ecx \n\t"
"movl $0,%%ebx \n\t"
"movl $4,%%eax \n\t"
"int $0x80 \n\t"
::"r"(str):"edx","ecx","ebx");
}
void exit()
{
asm( "movl $42,%ebx \n\t"
"movl $1,%eax \n\t"
"int $0x80 \n\t" );
}
void nomain()
{
print();
exit();
}

write 原型 int write(int filedesc, char*buffer, int size);

系统调用 通过 0x80中断实现

eax 表示 调用号,ebx,ecx,edx 用来传递参数

write 调用号为 4 ,eax = 4

filedesc 为要写入的文件句柄,向stdout输出,ebx = 0

buffer 标识要写入的缓冲区地址,要输出字符串 str, ecx = str

size 表示要写入的字节数,edx = 13

exit

exit 系统调用号为 1 eax = 1

ebx 表示进程退出码,本代码中 ebx = 42

直接编译gcc -c -fno-builtin nomain.c -o nomain.o

出现报错,问题原因:
在64位系统下去编译32位的目标文件,这样是非法的。

解决方案:
用”-m32”强制用32位ABI去编译,即可编译通过。

1
$gcc -c -fno-builtin -m32 TinyHelloWorld.c

链接ld -static -e nomain -o nomain nomain.o

  • -fno-builtin GCC编译器提供了很多内置函数(Built-in Function),它会把一些常用的C库函数替换成编译器的内置函数,以达到优化的功能。比如GCC会将只有字符串参数的printf函数替换成puts,以节省格式解析的时间。exit()函数也是GCC的内置参数之一,所以我们要使用-fno-builtin参数来关闭GCC内置函数功能
  • -e 是指定程序入口函数为nomain();
  • -static 表示ld是静态链接的方式链接程序,而不是用默认的动态链接方式;
  • -o 表示指定输出文件名为”nomain”

也会报错,

输入目标文件`TinyHelloWorld.o’是32位系统的,然而我们的平台是64位的(默认链接脚本位于/usr/lib/ldscripts下,x86_64平台默认链接64位可执行文件用的是elf_x86_64.x,链接32位可执行文件用的是elf32_x86_64.x),如果直接ld肯定不匹配,所以需要指定链接脚本与输入目标文件对应的。

解决方案:
链接的时候加上“-m elf_i386”,因为输入目标文件为i386平台。

原文链接:https://blog.csdn.net/neuq_jtxw007/article/details/78112672

1
ld -static -m elf_i386 -e nomain -o nomain nomain.o

运行

结果如下,退出码也是正确的:

使用链接脚本

1
2
3
4
5
6
7
ENTRY(nomain)
SECTIONS
{
. = 0x08048000 + SIZEOF_HEADERS;
tinytext : { *(.text) *(.data) *(.rodata) }
/DISCARD/ : { *(.comment) }
}

似乎能正常运行:

链接后各段:

链接脚本语法

链接脚本由语句构成,分为 命令语句和赋值语句。语法与 C 语言类型,以下几点都相似:

赋值语句:略

命令语句:一般格式是由一个关键字和紧跟其后的参数所组成,如 ENTRY 命令和 SECTION 命令。一些常见命令如下表:

如下:通过 ld -verbose 显示的默认链接脚本包含以下内容,所以才可以在文件中引用

SECTION 命令:基本形式

1
2
3
4
5
6
SECTIONS
{
...
secname : { contents }
...
}

secname表示输出段的段名,secname后面必须有一个空格符,这样使得输出段名不会有歧义,后面紧跟着冒号和一对大括号。大括号里面的contents描述了一套规则和条件,它表示符合这种条件的输入段将合并到这个输出段中。

contents中可以包含若干个条件,每个条件之间以空格隔开,如果输入段符合这些条件中的任意一个即表示这个输入段符合contents规则。条件的写法如下:filename(sections), filename表示输入文件名,sections表示输入段名。让我们举几个条件的例子来看看:

  1. file1.o(.data) 表示输入文件中名为file1.o的文件中名叫.data的段符合条件。
  2. file1.o(.data .rodata) 或 file1.o(.data, .rodata) 表示输入文件中名为file1.o的文件中的名叫.data或.rodata的段符合条件。
  3. *(.data) 所有输入文件中的名字为.data的文件符合条件。 * 是通配符,类似于正则表达式中的 *,我们还可以使用正则表达式中的 ?、[]等规则。
  4. [a-z](.text*[A-Z]) 这个条件比较复杂,它表示所有输入文件中以小写字母a到z开头的文件中所有段名以.text开头,并且以大写字母A到Z结尾的段。从这个规则中你也许可以看到一些链接脚本功能的强大。很明显,当我们回头再看TinyHelloWorld.lds链接脚本,发现它的SECTIONS命令中除了有一条赋值语句之外,还有两条段规则,相信你能够很快地根据上面给出的条件做出定义分析。