链接LINK 事实上属于非常基础的知识,但是真正理解的人并不多,包括目前的我。
共分为4章:
第一部分简介:
1.2 万变不离其宗
CPU 、内存、I/O 控制芯片 重要。I/O设备指的是显示设备、键盘、软盘、磁盘等。早期,CPU的核心频率和内存一样,都是通过IO控制芯片连接在同一个总线 BUS上的。这是因为早期的I/O设备速度慢,比如仅仅是字符终端。

后来,由于CPU频率提升,为协调CPU、内存、高速图形设备的关系,设计了高速北桥芯片。如果那些低速的设备如磁盘USB键盘也直接放到北桥,那么北桥就会相当复杂,于是专门设计了南桥用了放低速设备。20世纪90年代,系统总线是PCI,低速是ISA总线,
1.4.2 设备的驱动
在OS成熟以前,程序员需要同硬件打交道,比如A型号的显卡和B型号的显卡的接口完全不同。OS成熟以后,硬件被抽象成一系列概念。
1.5 & 1.6 很基础且重要,不过暂且先不看。
第二部分 编译和链接:
通常 编译+链接 = Build,事实上,分为预处理、编译、汇编、链接 4步。
2.1.1 预编译
根据文章内容,本博主写个例子:
main.c
#include "main.h"
int global = 2;
#if 0
void unusefulFun(void)
{
static int jjjjj = 12;
jjjjj++;
}
#endif
//after gcc 4.4
#pragma GCC push_options
#pragma GCC optimize ("O0")
static void hello_2(void)
{
global++;
}
#pragma GCC pop_options
static void hello(void)
{
global += NIHAO;
hello_2();
}
int main(void)
{
hello();
}
main.h
void hello(void);
#define NIHAO 100
然后 用$ arm-none-eabi-gcc -E main.c -o main.i
得到main.i,如下:
# 1 "main.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "main.c"
# 1 "main.h" 1
void hello(void);
# 4 "main.c" 2
int global = 2;
# 15 "main.c"
#pragma GCC push_options
#pragma GCC optimize ("O0")
static void hello_2(void)
{
global++;
}
#pragma GCC pop_options
static void hello(void)
{
global += 100;
hello_2();
}
int main(void)
{
hello();
}
关于生成的行号,文章这么写的“添加行号和文件名标识,以便于编译时编译器产生调试用的行号信息及用于编译时产生编译错误或警告时能够显示行号”。
2.1.2 编译
编译过程就是把预处理完的文件进行一系列词法分析、语法分析、语义分析及优化后生产相应的汇编代码文件,这个过程往往是我们所说的整个程序构建的核心部分,也是最复杂的部分之一。
然后:
$ arm-none-eabi-gcc -S main.i -o main.s
main.c:24:13: error: static declaration of 'hello' follows non-static declaration
24 | static void hello(void)
| ^~~~~
In file included from main.c:3:
main.h:3:6: note: previous declaration of 'hello' was here
3 | void hello(void);
| ^~~~~
可见代码有个bug。就是函数有static,但是这个staic的函数被放到了.h里面。
于是修改代码把static void hello(void) 改为void hello(void)
于是 arm-none-eabi-gcc -E main.c -o main.i 然后arm-none-eabi-gcc -S main.i -o main.s (当然也可以这两步合为一句 arm-none-eabi-gcc -S main.c -o main.s,这样就直接生成main.s, 中间的main.i看不到了 )
得到main.s 如下:
.cpu arm7tdmi
.eabi_attribute 20, 1
.eabi_attribute 21, 1
.eabi_attribute 23, 3
.eabi_attribute 24, 1
.eabi_attribute 25, 1
.eabi_attribute 26, 1
.eabi_attribute 30, 6
.eabi_attribute 34, 0
.eabi_attribute 18, 4
.file "main.c"
.text
.global global
.data
.align 2
.type global, %object
.size global, 4
global:
.word 2
.text
.align 2
.arch armv4t
.syntax unified
.arm
.fpu softvfp
.type hello_2, %function
hello_2:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
str fp, [sp, #-4]!
add fp, sp, #0
ldr r3, .L2
ldr r3, [r3]
add r3, r3, #1
ldr r2, .L2
str r3, [r2]
nop
add sp, fp, #0
@ sp needed
ldr fp, [sp], #4
bx lr
.L3:
.align 2
.L2:
.word global
.size hello_2, .-hello_2
.align 2
.global hello
.syntax unified
.arm
.fpu softvfp
.type hello, %function
hello:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
push {fp, lr}
add fp, sp, #4
ldr r3, .L5
ldr r3, [r3]
add r3, r3, #100
ldr r2, .L5
str r3, [r2]
bl hello_2
nop
sub sp, fp, #4
@ sp needed
pop {fp, lr}
bx lr
.L6:
.align 2
.L5:
.word global
.size hello, .-hello
.align 2
.global main
.syntax unified
.arm
.fpu softvfp
.type main, %function
main:
@ Function supports interworking.
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
push {fp, lr}
add fp, sp, #4
bl hello
mov r3, #0
mov r0, r3
sub sp, fp, #4
@ sp needed
pop {fp, lr}
bx lr
.size main, .-main
.ident "GCC: (GNU Arm Embedded Toolchain 9-2020-q2-update) 9.3.1 20200408 (release)"
2.1.3 汇编
汇编器是把汇编代码转变成机器可以执行的指令,汇编相对比较简单。
然后arm-none-eabi-gcc -c main.s -o main.o,得到main.o
用ultraEdit打开得到:
2.1.4 链接:
链接通过是一个让人比较费解的过程。也是本书的主要内容。
2.2 编译器做了什么(词法分析-语法分析-语义分析-中间语言生成-目标代码生成与优化)
2.3 链接器的年龄比编译器长
博主总结:汇编的程序发生变化,就需要低级的繁琐的调整相关地址。现代软件开发规模庞大,模块化开发更利于阅读、理解、重用代码,因此这些模块之间最后组合成单一的程序。这些模块拼接的过程就是本书的一个主题:链接。
2.4 模块拼装---静态链接
链接的过程就是把各个模块之间相互引用的部分都处理好,使得各个模块之间能够正确的衔接。链接过程主要包括了地址和空间分配(Address and Storage Allocation)、符号决议(Symbol Resolution)和重定位(Relocation)等这些步骤。
最基本的静态连接过程为,将米格模块的源代码文件(如.C)文件经过编译器编译成目标文件(.Obj),目标文件和库(Library)一起链接形成最终可执行文件。
第三章 目标文件里有什么
目标文件是源代码编译后但未进行链接的那些中间文件(Windows的obj和Linux的.o)
3.1 目标文件的格式
PC平台流行的可执行文件格式主要是windows下的PE以及linux的ELF(Executable Linkable Format)。目标文件(.obj .o)跟可执行文件的内容与结构很相似。。广义上看,目标文件和可执行文件可以看成是一种类型的文件。
3.2 目标文件是什么样的
目标文件里的内容除了编译后的机器指令代码、数据外,还有链接时需要的一些信息,如符号表、调试信息、字符串等。一般目标文件将这些信息按不同的属性,以“节(Section)”的形式存储,有时候也叫做“段”(Segment)。
代码段属于程序指令,数据段和.BSS属于程序数据。为什么要把程序指令和程序数据分开存放?
好处有:
(1)数据可以读写,而程序只读。放在不同区域可以防止程序的指令被有意或者无意改变。
(2)由于现代的缓存Cache被设计为数据缓存和指令缓存分离,所以程序指令和数据分开存放有利于CPU的缓存命中率。
(3)最重要的原因:当多个程序副本指令一样时.....
3.3 挖掘SimpleSection.o
真正了不起的程序员堆自己的程序的每一个字节都了如指掌。
$ arm-none-eabi-objdump -h main.o
main.o: file format elf32-littlearm
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000084 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 000000b8 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 000000bc 2**0
ALLOC
3 .comment 0000004d 00000000 00000000 000000bc 2**0
CONTENTS, READONLY
4 .ARM.attributes 0000002a 00000000 00000000 00000109 2**0
CONTENTS, READONLY
$ arm-none-eabi-objdump -x main.o
main.o: file format elf32-littlearm
main.o
architecture: armv4t, flags 0x00000011:
HAS_RELOC, HAS_SYMS
start address 0x00000000
private flags = 5000000: [Version5 EABI]
Sections:
Idx Name Size VMA LMA File off Algn
0 .text 00000084 00000000 00000000 00000034 2**2
CONTENTS, ALLOC, LOAD, RELOC, READONLY, CODE
1 .data 00000004 00000000 00000000 000000b8 2**2
CONTENTS, ALLOC, LOAD, DATA
2 .bss 00000000 00000000 00000000 000000bc 2**0
ALLOC
3 .comment 0000004d 00000000 00000000 000000bc 2**0
CONTENTS, READONLY
4 .ARM.attributes 0000002a 00000000 00000000 00000109 2**0
CONTENTS, READONLY
SYMBOL TABLE:
00000000 l df *ABS* 00000000 main.c
00000000 l d .text 00000000 .text
00000000 l d .data 00000000 .data
00000000 l d .bss 00000000 .bss
00000000 l F .text 00000030 hello_2
00000000 l d .comment 00000000 .comment
00000000 l d .ARM.attributes 00000000 .ARM.attributes
00000000 g O .data 00000004 global
00000030 g F .text 00000034 hello
00000064 g F .text 00000020 main
RELOCATION RECORDS FOR [.text]:
OFFSET TYPE VALUE
00000028 R_ARM_V4BX *ABS*
0000002c R_ARM_ABS32 global
0000005c R_ARM_V4BX *ABS*
00000060 R_ARM_ABS32 global
0000006c R_ARM_CALL hello
00000080 R_ARM_V4BX *ABS*
$ arm-none-eabi-size main.o
text data bss dec hex filename
132 4 0 136 88 main.o
3.3.1
$ arm-none-eabi-objdump -s -d main.o
main.o: file format elf32-littlearm
Contents of section .text:
0000 04b02de5 00b08de2 1c309fe5 003093e5 ..-......0...0..
0010 013083e2 10209fe5 003082e5 0000a0e1 .0... ...0......
0020 00d08be2 04b09de4 1eff2fe1 00000000 ........../.....
0030 00482de9 04b08de2 20309fe5 003093e5 .H-..... 0...0..
0040 643083e2 14209fe5 003082e5 ebffffeb d0... ...0......
0050 0000a0e1 04d04be2 0048bde8 1eff2fe1 ......K..H..../.
0060 00000000 00482de9 04b08de2 feffffeb .....H-.........
0070 0030a0e3 0300a0e1 04d04be2 0048bde8 .0........K..H..
0080 1eff2fe1 ../.
Contents of section .data:
0000 02000000 ....
Contents of section .comment:
0000 00474343 3a202847 4e552041 726d2045 .GCC: (GNU Arm E
0010 6d626564 64656420 546f6f6c 63686169 mbedded Toolchai
0020 6e20392d 32303230 2d71322d 75706461 n 9-2020-q2-upda
0030 74652920 392e332e 31203230 32303034 te) 9.3.1 202004
0040 30382028 72656c65 61736529 00 08 (release).
Contents of section .ARM.attributes:
0000 41290000 00616561 62690001 1f000000 A)...aeabi......
0010 05345400 06020801 09011204 14011501 .4T.............
0020 17031801 19011a01 1e06 ..........
Disassembly of section .text:
00000000 <hello_2>:
0: e52db004 push {fp} ; (str fp, [sp, #-4]!)
4: e28db000 add fp, sp, #0
8: e59f301c ldr r3, [pc, #28] ; 2c <hello_2+0x2c>
c: e5933000 ldr r3, [r3]
10: e2833001 add r3, r3, #1
14: e59f2010 ldr r2, [pc, #16] ; 2c <hello_2+0x2c>
18: e5823000 str r3, [r2]
1c: e1a00000 nop ; (mov r0, r0)
20: e28bd000 add sp, fp, #0
24: e49db004 pop {fp} ; (ldr fp, [sp], #4)
28: e12fff1e bx lr
2c: 00000000 .word 0x00000000
00000030 <hello>:
30: e92d4800 push {fp, lr}
34: e28db004 add fp, sp, #4
38: e59f3020 ldr r3, [pc, #32] ; 60 <hello+0x30>
3c: e5933000 ldr r3, [r3]
40: e2833064 add r3, r3, #100 ; 0x64
44: e59f2014 ldr r2, [pc, #20] ; 60 <hello+0x30>
48: e5823000 str r3, [r2]
4c: ebffffeb bl 0 <hello_2>
50: e1a00000 nop ; (mov r0, r0)
54: e24bd004 sub sp, fp, #4
58: e8bd4800 pop {fp, lr}
5c: e12fff1e bx lr
60: 00000000 .word 0x00000000
00000064 <main>:
64: e92d4800 push {fp, lr}
68: e28db004 add fp, sp, #4
6c: ebfffffe bl 30 <hello>
70: e3a03000 mov r3, #0
74: e1a00003 mov r0, r3
78: e24bd004 sub sp, fp, #4
7c: e8bd4800 pop {fp, lr}
80: e12fff1e bx lr
4.5静态库链接
查看.a 里面有什么?
找到安装目录,arm-none-eabi-ar -t libc.a
4.6.1 链接控制脚本
arm-none-eabi-ld -verbose 查看默认链接脚本
4.6.3 使用ld链接脚本
目标文件、库文件就是输入,链接结果输出的可执行文件就是输出。
把输入文件中的段称为输入段,输出文件中的段称为输出段。简单来讲,控制链接的过程无非是控制输入段如何变成输出段,比如哪些输入段要合并一个输出段,哪些输入段要丢弃;指定输出段的名字、装载地址、属性等等。
第一句 意思是设置当前的虚拟地址。就是tinytext的起始虚拟地址是0x0804800 + SIZEOF_HEADERS。
第二句是段转换规则,就是将所有输入文件的名字为".text"、“.data”、“.rodata”的段依次合并到输出文件的tinytext。
4.6.4 ld链接脚本的语法
链接脚本语句分两种,一种是命令语句,一种是赋值语句。
(1)赋值语句必须以;作为分隔符,命令语句可以用换行作为分隔符
(2)表达式、运算符 同C语言一样
(3)是注释,包含分号的,应该用双引号引用起来。
secname是输出段的段名,secname后面必须有一个空格符,这样使得输出段名不会有歧义。contents描述了一套规则和条件。