第三章 C语言基础
1. Introduction
2. C language: General terms and concepts
- C程序的构建过程:编译、汇编、连接
- 编译:预处理器处理c文件和h文件,展开宏;编译器把预处理的文件翻译为汇编语言
- 汇编:汇编器把上一步得到的汇编文件组合为目标文件,例如ELF,在连接器连接所有的目标文件之 前,所有的目标文件称为可重定位目标文件
- 连接:连接器把所有的可重定位目标文件组合在一起,构建最终的可执行文件
- 加载:Linux内核使用ELF加载器读取ELF文件并将其加载到内存
3. C objects and identifiers: Terms and Concepts
- C语言中的对象是一个数据存储区域。由一个标识符唯一确定,每一个标识符有一个作用域,标明了该数据的可见范围。C语言中有三种作用域:函数域,文件域,块作用域
int a; // scope: file
static int b; // scope: file
void main(int argc, int argv[]){ // argv and argc: function scope
int a; // scope: block. Hides the previous a (which has file scope)
...
}
不同的作用域,其生存时间也不同。在该对象所属的部分执行完成后,对象就被销毁。若想要改变生存时间和可见性,应加上修饰符号。有两个储存类说明符:extern和statistic。
所有声明为 static 或 extern 具有静态存储持续时间的对象。linkage意味着,该变量可以在其他范围内使⽤:内部linkage意味着该对象可以在当前C/h文件的所有范围内使⽤,外部linkage意味着该变量甚⾄可以在其他c文件的范围内使⽤。
static int a; // storage duration: static, linkage: internal
extern int b; // storage duration: static, linkage: external
int c; // storage duration: static, linkage: external
void main(void){
static int d; // storage duration: static, linkage: no linkage
extern int e; // storage duration: static, linkage: external
int f; // local only
}
4. Compiler, Assembler and Linker: Terms and concepts
节,是存储区域中的地址范围。一个部分中的所有数据都保存着用于某些特定目的的数据。
节这一概念在可重定位对象文件和可执行对象文件级别上都被使用。所有来自一个C文件的机器代码和数据都以ELF文件的格式,按节存储。在链接步骤中,即当连接可执行对象文件时,这些输入部分(从可重定位对象文件输入)被连接到输出部分(输出到可执行对象文件)。目标文件的哪些部分被连接到可执行文件的哪些部分被定义在链接器脚本中。
编译器将C代码翻译成汇编代码,在汇编代码中定义这些符号和节。汇编程序将汇编程序代码翻译成机器码,并构建一个目标文件(在本例中为ELF)。在汇编程序中,符号被用作各种符号的总称:指令,汇编指令和标签,都是符号。只有汇编程序中的一些符号会被放入目标文件的符号表中。
符号的类型由特殊字符决定:⼀个 ”.” 前⾯的⼀个符号确定该符号是⼀个指令。⼀个“:”跟在⼀个符号之后,定义了⼀个标签
label1: ; this is a label
.word variable1 ; this is the directive .word, defining a variable
add R1, #1 ; this is a assembly instruction
5. ELF
ELF文件格式:
ELF文件的生命周期中,执行两项任务:在构建过程中构造数据和机器代码,这在构建过程的链接步骤中尤为重要;⽤于执⾏的结构数据和机器代码。这造成了ELF ⽂件的执⾏视图和链接视图两种不同的使用。
在连接视图中,ELF文件被分为节,每一个节都有一个节名;在执行视图中,ELF文件被分为段。段是⼀个或具有相同功能的多个节的集合,每个段都有⼀个程序头。段头定义了执⾏ ELF ⽂件的元数据。对于链接视图,段是不相关的。
在ELF文件有一个头部,指明了ELF文件的一些信息。其中比较重要的有:
- 类型 ( e_type ):指定提到的类型:
- 可重定位的⽬标⽂件 ( ET_REL )
- 可执⾏⽬标⽂件 ( ET_EXEC )
- 共享对象⽂件 ( ET_DYN )
- ⼊⼝点:(e_entry):指定⽬标⽂件开始执⾏的位置。这仅适⽤于 ET_EXEC。
是 bit[0] 地址为 1,⼊⼝点包含 Arm Thumb 代码,0 表示 Arm 代码。 - 查找节头表的详细信息:偏移量 ( e_shoff ) 和条⽬数 ( e_shnum )。
- 查找程序头表的详细信息:偏移量 ( e_phoff ) 和条⽬数 ( e_phnum )。
可重定位ELF文件头示例:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: ARM
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 1008 (bytes into file)
Flags: 0x5000000, Version5 EABI
Size of this header: 52 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 40 (bytes)
Number of section headers: 10
Section header string table index: 9
可执行ELF文件头示例:
ELF Header:
Magic: 7f 45 4c 46 01 01 01 00 00 00 00 00 00 00 00 00
Class: ELF32
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: EXEC (Executable file)
Machine: ARM
Version: 0x1
Entry point address: 0x20000001
Start of program headers: 52 (bytes into file)
Start of section headers: 132028 (bytes into file)
Flags: 0x5000200, Version5 EABI, soft-float ABI
Size of this header: 52 (bytes)
Size of program headers: 32 (bytes)
Number of program headers: 2
Size of section headers: 40 (bytes)
Number of section headers: 10
Section header string table index: 9
ARM 处理器有两个不同的指令集,⼀个较⼤的称为 ARM,⼀个较⼩的⼦集称为 Thumb。为了区分这两种模式,使⽤了地址的位[0]。如果设置为 0,代码将在 ARM 模式下执⾏,如果设置为 1,代码将在 Thumb 模式下执⾏。在这种情况下,⼊⼝点地址的 Bit[0] 为 1,因此处理器将以Thumb 模式启动。编译器使⽤指令.thumb说明了这一点。
可执行文件中每个段的属性在程序头中被定义,示例如下:
Program Headers:
Type Offset VirtAddr PhysAddr FileSiz MemSiz Flg Align
LOAD 0x010000 0x01000000 0x01000000 0x0009c 0x000a4 RWE 0x10000
LOAD 0x020000 0x20000000 0x20000000 0x00016 0x00016 R E 0x10000
6. Symbols
符号是⼀个⾮常核⼼的概念。它们基本上是⽬标⽂件中数据和代码的名称或引⽤,它们以某种形式被构建过程的每⼀步使⽤。即使在调试代码时的构建过程之后,符号也⽤于命名代码和数据。
ELF 定义了⼀个符号表,其中所有符号都存储在⼀个列表中。每个符号都有⼀个绑定,表示它们在 C文件 中的可⻅性,但在对象⽂件级别上。
6.1 符号:⼯具
检查 ELF ⽂件符号表的⼯具有:
- nm 或他们的 Arm 等价物,例如 arm-none-eabi-nm
- readelf 或者 arm-none-eabi-readelf
在 nm 定义符号的部分中,符号名称前⽤⼀个字符表示。⼩写表示符号是局部的,⼤写表示符号是全局的。
实验:
arm-none-eabi-gcc -c -mcpu=cortex-m33 -Os -nostdlib main11.c
编译以下代码。
int global_linkage_file_scope_var = 1;
extern int extern_file_scope_var;
static int static_file_scope_defined_var = 3;
extern int extern_function(int p1);
int main(){
return extern_function( extern_file_scope_var) + extern_function(static_file_scope_defined_var);
}
arm-none-eabi-nm main11.o
来查看编译完成的o文件的符号表
6.2 符号:ELF
每个可重定位目标模块m都有一个符号表,它包含了在m中定义和引用的符号。有三种链接器符号:
-
Global symbols(模块内部定义的全局符号)
由模块m定义并能被其他模块引用的符号。例如,非static C函数和非static C全局变量
如,main.c 中的全局变量名buf -
External symbols(外部定义的全局符号)
由其他模块定义并被模块m引用的全局符号
如,main.c 中的函数名swap -
Local symbols(本模块的局部符号)
仅由模块m定义和引用的本地符号。例如,在模块m中定义的带static的C函数和全局变量
如,swap.c中的static变量名bufp1.
注意:链接器的局部符号不是指程序中的局部变量(分配在栈中的临时性变量),链接>器不关心这种局部变量
符号定义的本质是:指被分配了存储空间。如果是函数名则指代码所在区;如果是变量名则指其所在的静态数据区。所有定义的符号的值就是其目标所在的首地址。因此,符号的解析就是将符号引用和符号定义建立关联后,将引用符号的地址重定位为相关联的符号定义的地址。
6.3 符号:编译器
无
7. Sections
7.1 Tools
arm-none-eabi-gcc -S -mcpu=cortex-m33 -Os -nostdlib main11.c
编译如下代码开始实验
int global_linkage_file_scope_var = 1;
extern int extern_file_scope_var;
static int static_file_scope_defined_var = 3;
extern int extern_function(int p1);
int main(){
int local_var = extern_function( extern_file_scope_var) + extern_function(static_file_scope_defined_var);
return local_var;
}
arm-none-eabi-readelf --sections a.out
使用这个命令来读取所有的elf节头
得到如下结果
There are 11 section headers, starting at offset 0x2dc:
Section Headers:
[Nr] Name Type Addr Off Size ES Flg Lk Inf Al
[ 0] NULL 00000000 000000 000000 00 0 0 0
[ 1] .text PROGBITS 00000000 000034 000000 00 AX 0 0 2
[ 2] .data PROGBITS 00000000 000034 000004 00 WA 0 0 4
[ 3] .bss NOBITS 00000000 000038 000000 00 WA 0 0 1
[ 4] .text.startup PROGBITS 00000000 000038 00001c 00 AX 0 0 4
[ 5] .rel.text.startup REL 00000000 00026c 000018 08 I 8 4 4
[ 6] .comment PROGBITS 00000000 000054 00007a 01 MS 0 0 1
[ 7] .ARM.attributes ARM_ATTRIBUTES 00000000 0000ce 000034 00 0 0 1
[ 8] .symtab SYMTAB 00000000 000104 000100 10 9 13 4
[ 9] .strtab STRTAB 00000000 000204 000066 00 0 0 1
[10] .shstrtab STRTAB 00000000 000284 000057 00 0 0 1
Key to Flags:
W (write), A (alloc), X (execute), M (merge), S (strings), I (info),
L (link order), O (extra OS processing required), G (group), T (TLS),
C (compressed), x (unknown), o (OS specific), E (exclude),
y (purecode), p (processor specific)
7.2 ELF
section头表是一个定义良好的结构数组(Elf32_Shdr)。这个数组的每个元素都包含一个section的信息。ELF头e_shoff值指向section头表的开始部分。因为有空节,所以节头可以在没有相应数据的情况下存在。每个section都有一个section头条目。每个section头有以下字段:
- sh_name:节名。
- sh_addr:保存一个section在执行后被加载到内存中的地址。这对于可执行的对象文件显然是很重要的,在可重定位的对象文件中大部分是零,正如名称——可重定位——已经暗示的那样。
- sh_offset: ELF文件中section内容开始时的偏移量。
- sh_size:文件中的字节数。
常见的section如下:
Name | sh_type | Description |
---|---|---|
.bss | SHT_NOBITS | uninitialized data |
.data / data1 | SHT_PROGBITS | initialized data |
.text | SHT_PROGBITS | executable instructions, machine code |
.rel$name | SHT_REL | Holds relocation entries for the section $name, could be .rel.text for example. |
.symtab | SHT_SYMTAB | Holds the symbol table |
7.3 Compiler and Assembler
- .data节,用于C对象,例如变量,它们有静态的存储时间并且初始化一个非零值。当对象被声明为static或extern时,它们具有静态存储持续时间。作用域(例如文件作用域或块作用域)与存储时间无关。
使用arm-none-eabi-gcc -S -mcpu=cortex-m33 -nostdlib data1.c
命令编译实验代码
static int a = 1;
int main(void){
return a;
}
得到汇编代码
.cpu cortex-m33
.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, 1
.eabi_attribute 18, 4
.file "data1.c"
.text
.data
.align 2
.type a, %object
.size a, 4
a:
.word 1
.text
.align 1
.global main
.arch armv8-m.main
.arch_extension dsp
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
add r7, sp, #0
ldr r3, .L3
ldr r3, [r3]
mov r0, r3
mov sp, r7
@ sp needed
pop {r7}
bx lr
.L4:
.align 2
.L3:
.word a
.size main, .-main
.ident "GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"
.data指令指示汇编程序将下列所有指令汇编到.data节中。在使用标签a:定义符号a之后,指令.word被用来将值1写入.data部分。同样的.type a, %object被用来在object文件的符号表中将符号a定义为对象类型。
在块作用域中声明静态存储时间的非零初始化对象也被添加到.data节中
- .bss节。与data节类似,编译代码。
static int a;
int main(void){
return a;
}
得到汇编代码如下
.cpu cortex-m33
.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, 1
.eabi_attribute 18, 4
.file "bss1.c"
.text
.bss
.align 2
a:
.space 4
.size a, 4
.text
.align 1
.global main
.arch armv8-m.main
.arch_extension dsp
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
add r7, sp, #0
ldr r3, .L3
ldr r3, [r3]
mov r0, r3
mov sp, r7
@ sp needed
pop {r7}
bx lr
.L4:
.align 2
.L3:
.word a
.size main, .-main
.ident "GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"
该指令 .bss 指示汇编器将以下所有指令汇编到名为.bss的部分中。或者, .section .bss 可以使⽤该指令。该指令 .section 允许任意部分名称,其中 .bss 部分名称始终为.bss。标签 a: 定义符号并 .space 4 保留 4 个字节,全部组装到.bss节中。
在块作⽤域中声明的具有静态存储持续时间的未初始化对象也被添加到.bss部分。这同样适⽤于初始化为零并具有静态存储持续时间的对象。
- Uninitialized objects with external linkage
在之前对.bss部分的解释中,示例仅显示了⼀个具有内部链接的对象:已 static 声明的对象。当声明具有外部链接的对象时——没有 static 关键字的⽂件范围对象具有外部链接和静态存储持续时间——但未初始化,编译器和汇编器必须以特定⽅式处理这些。想象⼀下编译器运⾏多个⽂件。他们每个⼈都⽤外部链接声明了相同的⽂件范围对象,但他们都没有初始化它。由于这些⽂件中的每⼀个都是⼀个翻译单元的⼀部分,因此编译器和汇编器现在都不会在编译和汇编时将这些未定义的对象(.bss 或 .data?)放置在哪个部分。只有当编译器遇到声明和初始化对象的⽂件时,它才知道:这应该放在.data 中。但是,如果没有翻译单元初始化对象,则必须在 .bss 中保留空间!
arm-none-eabi-gcc -S -c -mcpu=cortex-m33 -nostdlib bss2.c
编译代码
int c;
int main(void){
return c;
}
得到汇编代码
.cpu cortex-m33
.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, 1
.eabi_attribute 18, 4
.file "bss2.c"
.text
.comm c,4,4
.align 1
.global main
.arch armv8-m.main
.arch_extension dsp
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
add r7, sp, #0
ldr r3, .L3
ldr r3, [r3]
mov r0, r3
mov sp, r7
@ sp needed
pop {r7}
bx lr
.L4:
.align 2
.L3:
.word c
.size main, .-main
.ident "GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"
指令.comm声明了一个公共符号。在链接所有对象中具有相同名称的所有公共或定义符号时,文件合并为一个。当链接器没有遇到具有相同名称的已定义符号时,在.bss节中保留空格。在上面的例子中。comm c,4,4将保留4个字节。
variable is … | handling in translation unit |
---|---|
initialized and static storage duration (static or file-scope objects) | .data |
uninitialized and static storage duration (static or file-scope objects) | .bss |
external linkage | .comm directive ends up in .data or .bss after linking depended on the initialization status in other translation units. |
8. Literal Pools
literal pool的本质就是ARM汇编语言代码节中的一块用来存放常量数据而非可执行代码的内存块。
当想要在一条指令中使用一个 4字节长度的常量数据(这个数据可以是内存地址,也可以是数字常量)的时候,由于ARM指令集是定长的(ARM指令4字节或Thumb指令2字节),所以就无法把这个4字节的常量数据编码在一条编译后的指令中。此时,ARM编译器(编译C源程序)/汇编器(编译汇编程序) 就会在代码节中分配一块内存,并把这个4字节的数据常量保存于此,之后,再使用一条指令把这个4 字节的数字常量加载到寄存器中参与运算。
在C源代码中,文字池的分配是由编译器在编译时自行安排的,在进行汇编程序设计时,开发者可以自己进行文字池的分配,如果开发者没有进行文字池的安排,那么汇编器就会代劳。
arm-none-eabi-gcc -S -mcpu=cortex-m33 -Os -nostdlib main12.c
编译代码
extern int externVar1;
extern int externVar2;
int main(){
return externVar1 + externVar2;
}
int myFunction(int a){
return externVar1 + a;
}
得到汇编代码
.cpu cortex-m33
.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, 1
.eabi_attribute 18, 4
.file "main12.c"
.text
.align 1
.global main
.arch armv8-m.main
.arch_extension dsp
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type main, %function
main:
@ args = 0, pretend = 0, frame = 0
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
add r7, sp, #0
ldr r3, .L3
ldr r2, [r3]
ldr r3, .L3+4
ldr r3, [r3]
add r3, r3, r2
mov r0, r3
mov sp, r7
@ sp needed
pop {r7}
bx lr
.L4:
.align 2
.L3:
.word externVar1
.word externVar2
.size main, .-main
.align 1
.global myFunction
.syntax unified
.thumb
.thumb_func
.fpu softvfp
.type myFunction, %function
myFunction:
@ args = 0, pretend = 0, frame = 8
@ frame_needed = 1, uses_anonymous_args = 0
@ link register save eliminated.
push {r7}
sub sp, sp, #12
add r7, sp, #0
str r0, [r7, #4]
ldr r3, .L7
ldr r2, [r3]
ldr r3, [r7, #4]
add r3, r3, r2
mov r0, r3
adds r7, r7, #12
mov sp, r7
@ sp needed
pop {r7}
bx lr
.L8:
.align 2
.L7:
.word externVar1
.size myFunction, .-myFunction
.ident "GCC: (GNU Tools for Arm Embedded Processors 9-2019-q4-major) 9.2.1 20191025 (release) [ARM/arm-9-branch revision 277599]"
标签. l3和. l7后面的指令是文字池。前者是main()的字面值池,包含externVar1和externVar2,后者是myFunction()的字面值池,包含externVar1。两者都不保存externVar1或externVar2的值,而是对实际值的引用。
出现文字池的原因是ldr指令自身只有8位
9. Relocation
重定位是链接过程的⼀部分,它将符号引⽤与符号定义连接起来。
在第 ( 3.7 )节的章节中,您可能已经注意到⼀个名为的特殊节 .rel$name (例如 .rel.text 代表节.text)。这些部分具有 sh_type SHT_REL 并保存部分name的重定位表条⽬。每个重定位表条⽬都描述了必须如何修改特定的可重定位⽬标⽂件 - 即如何将符号引⽤与符号定义连接 - 以成功链接并最终执⾏可执⾏⽬标⽂件。
在链接期间,定义了这些符号的部分将被放置到内存区域中。之后他们的地址将可⽤。连同被重新定位的地⽅的偏移量。链接器可以解析引⽤并将正确的值放⼊被重新定位的地⽅。
10. Veneers
与编译阶段受ldr指令影响类似,在连接阶段也被分支指令所影响。例如Armv8-M 中的分⽀指令,只有11 位或 8 位bit来表示地址。这限制了分⽀的范围。如果链接器遇到分⽀过远的情况,则插⼊veneers来解决。
venners是一段添加的代码,在venners这段代码里存储了跳转到原本跳不到的的地址(文字池技术)。通过这段代码,修改IP寄存器的值,从而直接跳到了目标位置。通过这项技术实现了在32位地址空间上的随意跳转。
当函数 A 调用函数 B,并且当这两个函数相距太远而bl命令无法表达时,链接器会在函数 A 和 B 之间插入函数 C,使得函数 C 接近函数 A。现在函数 A 使用b指令转到函数 C(在函数调用之间复制所有寄存器),函数 C 使用bl指令(也复制所有寄存器)。A对单板执行bl,单板将r12(IP)设置为最终目的地(B)并执行bx r12。bx 可以到达整个地址空间
11. 链接脚本
用途:链接器根据链接脚本的描述来正确链接可重定位文件
- 将输⼊节(从可重定位⽬标⽂件)映射到输出节(在可执⾏⽬标⽂件中)
- 将输出部分映射到内存区域
- 定义可执⾏⽬标⽂件的⼀些其他属性。
MEMORY {
RAMMAIN : ORIGIN = 0x01000000, LENGTH = 32K
RAMLIB : ORIGIN = 0x20000000, LENGTH = 32K
}
SECTIONS {
ENTRY(function_in_library)
.text : {
main11.o(.text)
} > RAMMAIN
.lib : {
main11-lib.o(.text)
} > RAMLIB
.data : {
*(.data)
} > RAMMAIN
}
ubuntu上搭建ARM虚拟环境
- 安装qemu模拟器
sudo apt-get install qemu
- 安装交叉编译工具链
sudo apt-get install gcc-arm-linux-gnueabihf
- 建立软连接防止动态库错误
sudo ln -s /usr/arm-linux-gnueabihf/lib/libc.so.6 /lib/libc.so.6
sudo ln -s /usr/arm-linux-gnueabihf/lib/ld-linux-armhf.so.3 /lib/ld-linux-armhf.so.3
make&makefile
1. 基本语法
target ... : prerequisites ...
command
...
- target: target也就是一个目标文件,可以是Object File,也可以是执行文件。还可以是一个标签(伪目标)
- prerequisites: prerequisites就是,要生成那个target所需要的文件或是目标。
- command: command也就是make需要执行的命令。(任意的Shell命令)
这是一个文件的依赖关系,也就是说,target这一个或多个的目标文件依赖于prerequisites中的文件,其生成规则定义在command中。说白一点就是说,prerequisites中如果有一个以上的文件比target文件要新的话,command所定义的命令就会被执行。这就是Makefile的规则。也就是Makefile中最核心的内容。
2. make如何工作
在默认的方式下,也就是我们只输入make命令。那么,
- make会在当前目录下找名字叫“Makefile”或“makefile”的文件。
- 如果找到,它会找文件中的第一个目标文件(target)并把这个文件作为最终的目标文件。
- 如果该文件不存在,或是所依赖的后面的 .o 文件的文件修改时间要比这个文件新,那么,他就会执行后面所定义的命令来生成这个文件。
- 如果所依赖的.o文件也存在,那么make会在当前文件中找目标为.o文件的依赖性,如果找到则再根据那一个规则生成.o文件。(这有点像一个堆栈的过程)
- 当然C文件和H文件一定得存在,于是make会生成 .o 文件,然后再用 .o 文件链接为执行文件。
这就是整个make的依赖性,make会一层又一层地去找文件的依赖关系,直到最终编译出第一个目标文件。在找寻的过程中,如果出现错误,比如最后被依赖的文件找不到,那么make就会直接退出,并报错,而对于所定义的命令的错误,或是编译不成功,make不会理会。make只管文件的依赖性,即,如果在找了依赖关系之后,冒号后面的文件还是不在,那就报错。
main11:main11.o main11-lib.o
arm-linux-gnueabi-gcc -o main main11.o main11-lib.o
main11.o:main11.c
arm-linux-gnueabi-gcc -c main11.c
main11-lib.o:main11-lib.c
arm-linux-gnueabi-gcc -c main11-lib.c
3.makefile中使用变量
在上面的例子中,先让我们看看edit的规则:
edit:main.o kbd.o command.o display.o insert.o search.o files.o utils.o
gcc -o edit main.o kbd.o command.o display.o insert.o search.o files.o utils.o
我们可以看到[.o]文件的字符串被重复了两次,如果我们的工程需要加入一个新的[.o]文件,那么我们需要在两个地方加(应该是三个地方,还有一个地方在clean中)。当然,我们的makefile并不复杂,所以在两个地方加也不累,但如果makefile变得复杂,那么我们就有可能会忘掉一个需要加入的地方,而导致编译失败。所以,为了makefile的易维护,在makefile中我们可以使用变量。makefile的变量也就是一个字符串,理解成C语言中的宏可能会更好。
比如,我们声明一个变量,叫objects, OBJECTS, objs, OBJS, obj, 或是 OBJ,反正不管什么啦,只要能够表示obj文件就行了。我们在makefile一开始就这样定义:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
于是,我们就可以很方便地在我们的makefile中以“$(objects)”的方式来使用这个变量了,于是我们的改良版makefile就变成下面这个样子:
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
gcc -o edit $(objects)
main.o : main.c defs.h
gcc -c main.c
kbd.o : kbd.c defs.h command.h
gcc -c kbd.c
command.o : command.c defs.h command.h
gcc -c command.c
display.o : display.c defs.h buffer.h
gcc -c display.c
insert.o : insert.c defs.h buffer.h
gcc -c insert.c
search.o : search.c defs.h buffer.h
gcc -c search.c
files.o : files.c defs.h buffer.h command.h
gcc -c files.c
utils.o : utils.c defs.h
gcc -c utils.c
clean :
rm edit $(objects)
于是如果有新的 .o 文件加入,我们只需简单地修改一下 objects 变量就可以了。
4.让make自动推导
GNU的make很强大,它可以自动推导文件以及文件依赖关系后面的命令,于是我们就没必要去在每一个[.o]文件后都写上类似的命令,因为,我们的make会自动识别,并自己推导命令。
只要make看到一个[.o]文件,它就会自动的把[.c]文件加在依赖关系中,如果make找到一个whatever.o,那么whatever.c,就会是whatever.o的依赖文件。并且 cc -c whatever.c 也会被推导出来,于是,我们的makefile再也不用写得这么复杂。我们的是新的makefile又出炉了。
objects = main.o kbd.o command.o display.o \
insert.o search.o files.o utils.o
edit : $(objects)
gcc -o edit $(objects)
main.o : defs.h
kbd.o : defs.h command.h
command.o : defs.h command.h
display.o : defs.h buffer.h
insert.o : defs.h buffer.h
search.o : defs.h buffer.h
files.o : defs.h buffer.h command.h
utils.o : defs.h
.PHONY : clean
clean :
rm edit $(objects)
这种方法,也就是make的“隐晦规则”。上面文件内容中,“.PHONY”表示,clean是个伪目标文件。
ARM汇编指令
一. 带点的(一般都是ARM GNU伪汇编指令)
- ".text"、".data"、".bss"
依次表示的是
“以下是代码段”,
“以下是初始化数据段”,
“以下是未初始化数据段”。
-
".global" 定义一个全局符号,通常是为ld使用。比如经常看到的.global _start
-
".ascii"、".byte"、".short"、".int"、".long"、".word"、".quad"
定义一个字符串,并为它分配空间
定义一个字节,并为它分配空间,占单字节,0x34
定义一个短整型,并为它分配空间,占双字节,0x1234
定义一个整型,并为它分配空间,占四字节,0x12345678
定义一个长整型,并为它分配空间,占四字节,0x12345678
定义一个字,并为它分配空间,
定义一个双字,并为它分配定义,占八字节,...
比如
.long 0x22011110//BWSCON
.long 0x00000700//BANKCON0
...
- ".abort"
停止汇编
- ".align"
.align absexpr1,absexpr2
以某种对齐方式,在未使用的存储区域填充值. 第一个值表示对齐方式,4, 8,16或32. 第二个表达式值表示填充的值
-
".if .else .endif"
-
".include"
.include "file":包含指定的头文件, 可以把一个汇编常量定义放在头文件中
- ".comm"
.comm symbol, length:
在bss段申请一段命名空间,该段空间的名称叫symbol, 长度为length. Ld连接器在连接会为它留出空间
- ".equ"
.equ symbol, expression:
把某一个符号(symbol)定义成某一个值(expression).该指令并不分配空间,相当于C语言中的#define。例如.equ aaa,0x20000000
- ".macro .endm"
.macro: 定义一段宏代码,.macro表示代码的开始,.endm表示代码的结束,.exitm跳出宏, 示例如下:
.macro SHIFTLEFT a, b
.if \b < 0
mov \a, \a, ASR #-\b
.exitm
.endif
mov \a, \a, LSL #\b
.endm
- ".req"
name .req register name: 为寄存器定义一个别名
- ".code"
.code [16|32]: 指定指令代码产生的长度, 16表示Thumb指令, 32表示ARM指令
- ".ltorg"
.ltorg: 表示当前往下的定义在归于当前段,并为之分配空间
二. 带下滑线的
1._start
汇编程序的缺省入口,但是可以更改,想要更改其他标志,到相应的链接脚本中去用ENTRY指明其他入口标志。标号可以直接认为是地址。
ELF头分解
成员 | 长度bytes | 含义 | 十六进制表示 |
---|---|---|---|
e_ident | 16 | Magic,类别,数据,版本,OS/ABI,ABI | 7F 45 4C 46 01 01 01 00 00 00 00 00 00 00 00 00 |
e_ident[0:3] | 4 | Magic Num | 7F 45 4C 46 |
e_ident[4] | 1 | 指示文件类型,是ELF32(0x01)还是ELF64(0x02)位 | 01 |
e_ident[5] | 1 | 指示文件的编码方式,是大端法(0x02)还是小端法(0x01) | 01 |
e_ident[6] | 1 | 标识ELF Version, 该值等于EV_CURRENT,目前为1 | 01 |
e_ident[7] | 1 | 表示着该文件运行的操作系统 | 00 |
e_ident[8] | 1 | 标志着 ABI (应用二进制接口)的版本,ABI相当于硬件层级的API | 00 |
e_ident[8:15] | 7 | 填充位,用零填充用以对齐,可以预留给未来使用 | 00 00 00 00 00 00 00 |
e_type | 2 | 类型 | 02 00 |
e_machine | 2 | 系统架构 | 28 00 |
e_version | 4 | 版本 | 01 00 00 00 |
e_entry | 4(32位)/8(64位) | 入口点地址 | 00 00 00 20 |
e_phoff | 4(32位)/8(64位) | start of program headers | 34 00 00 00 |
e_shoff | 4(32位)/8(64位) | start of section headers | C8 02 02 00 |
e_flags | 4 | 标志 | 00 02 00 05 |
e_ehsize | 2 | 文件头的大小 | 34 00 |
e_phentsize | 2 | 程序头大小 | 20 00 |
e_phnum | 2 | number of program headers | 03 00 |
e_shentsize | 2 | 节头大小 | 28 00 |
e_shnum | 2 | number of program headers | 0A 00 |
e_shstrndx | 2 | 字符串表段索引 | 09 00 |
程序头分解
由ELF头表得知有三个程序头表,以第一个为例
成员 | 含义 | 十六进制表示 |
---|---|---|
p_type | 表示程序头描述的段类型 | 01 00 00 00 |
p_offset | 表示段的第一字节相对文件头的偏移 | 00 00 01 00 |
p_vaddr | 物理地址 | 00 00 00 01 |
p_paddr | 虚拟地址 | 00 00 00 01 |
p_filesz | 段在文件中的长度 | 68 00 00 00 |
p_memsz | 段在内存中的长度 | 6C 00 00 00 |
p_flags | 与段相关的标志 | 07 00 00 00 |
p_align | 根据此项值来确定段在文件及内存中如何对齐 | 00 00 01 00 |
节头表
以.text节为例
成员 | 含义 | 十六进制表示 |
---|---|---|
sh_name | 表示节区名称(在.strtab的偏移) | 1B 00 00 00 |
sh_type | 表示节区类型 | 01 00 00 00 |
sh_flags | 表示该节在虚拟空间中的访问属性 | 06 00 00 00 |
sh_addr | 若可以被加载则对应的虚拟地址 | 00 00 00 01 |
sh_offset | 表示节区第一个字节相对文件头的偏移 | 00 00 01 00 |
sh_size | 表示节区的大小(单位字节) | 60 00 00 00 |
sh_link | 用于与链接相关 | 00 00 00 00 |
sh_info | 用于与链接相关 | 00 00 00 00 |
sh_addralign | 用于地址对齐 | 08 00 00 00 |
sh_entsize | 节中每个表项的长度,0表示不固定 | 00 00 00 00 |