-march=armv7-a:指定编译时arm架构(即代码要运行的架构)。
-mfloat-abi=softfp:soft/softfp/hard。
soft(软浮点):表明不是用FPU硬件,使用GCC整数算术库来模拟浮点运算
-mfpu=neon-vfpv4,参数-mfpu就是用来指定要产生那种硬件浮点运算指令。常用的有vfpv3,vfpv4,neon等,hi3536 A17支持的是neon+vfpv4相结合的结构。
模块是一种机制,通过这种机制,编程语言可以使用户将程序拆分为不同的逻辑部分。 模块化要求实现编程语言的工具提供一定程度的支持。 单独编译是实现此目的的机制。 在C语言中,程序可能会分解为几个源文件。 通常,编译C源文件会生成一个目标文件,因此,多个源文件将导致多个目标文件。 这些目标文件使用链接器进行组合。 链接器生成最终程序。
对于链接器,ELF可重定位文件是节的集合。 部分代表连续的数据块(可以是任何数据:指令,全局变量的初始值,调试信息等)。 每个部分都有一个名称和属性,例如是否必须在内存中分配,是否从映像(即包含程序的文件)中加载,是否可以执行,是否可写,大小和对齐方式等。
定义函数如下:
.balign 4
.text
.type mult_by_5,@function
.globl mult_by_5
mult_by_5:
add x0, x0, x0, LSL #2 /* r0 ← r0 + 4*r0 */
ret
进行汇编如下:
as -g -o mult_by_5.o mult_by_5.s
查看mult_by_5.o
objdump -dr mult_by_5.o
mult_by_5.o: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 :
0: 8b000800 add x0, x0, x0, lsl #2
4: d65f03c0 ret
主函数如下:
.data
.balign 4
message1: .asciz "Hey, type a number: "
.balign 4
message2: .asciz "%d times 5 is %d\n"
/* Format pattern for scanf */
.balign 4
scan_pattern : .asciz "%d"
/* Where scanf will store the number read */
.balign 4
number_read: .word 0
.balign 4
return: .word 0
.balign 4
return2: .word 0
.text
.global _start
_start:
ldr x0, address_of_message1 /* r0 ← &message1 */
bl printf /* call to printf */
ldr x0, address_of_scan_pattern /* r0 ← &scan_pattern */
ldr x1, address_of_number_read /* r1 ← &number_read */
bl scanf /* call to scanf */
ldr x0, address_of_number_read /* r0 ← &number_read */
ldr x0, [x0] /* r0 ← *r0 */
bl mult_by_5
mov x2, x0 /* r2 ← r0 */
ldr x1, address_of_number_read /* r1 ← &number_read */
ldr x1, [x1] /* r1 ← *r1 */
ldr x0, address_of_message2 /* r0 ← &message2 */
bl printf /* call to printf */
mov x8, 93
svc 0
address_of_message1 : .dword message1
address_of_message2 : .dword message2
address_of_scan_pattern : .dword scan_pattern
address_of_number_read : .dword number_read
address_of_return : .dword return
/* External */
.global printf
.global scanf
as -g -o print64-f.o print64-f.s
得到print64-f.o对象文件。这里看下address_of_message1 : .dword message1
,这个是需要冲定向的。
通过#objdump -dr print64-f.o
查看发现如下:
print64-f.o: file format elf64-littleaarch64
Disassembly of section .text:
0000000000000000 <_start>:
0: 580001e0 ldr x0, 3c
4: 94000000 bl 0
4: R_AARCH64_CALL26 printf
8: 58000220 ldr x0, 4c
c: 58000241 ldr x1, 54
10: 94000000 bl 0
10: R_AARCH64_CALL26 scanf
14: 58000200 ldr x0, 54
18: f9400000 ldr x0, [x0]
1c: 94000000 bl 0
1c: R_AARCH64_CALL26 mult_by_5
20: aa0003e2 mov x2, x0
24: 58000181 ldr x1, 54
28: f9400021 ldr x1, [x1]
2c: 580000c0 ldr x0, 44
30: 94000000 bl 0
30: R_AARCH64_CALL26 printf
34: d2800ba8 mov x8, #0x5d // #93
38: d4000001 svc #0x0
000000000000003c :
...
3c: R_AARCH64_ABS64 .data
0000000000000044 :
...
44: R_AARCH64_ABS64 .data+0x18
000000000000004c :
...
4c: R_AARCH64_ABS64 .data+0x2c
0000000000000054 :
...
54: R_AARCH64_ABS64 .data+0x30
000000000000005c :
...
5c: R_AARCH64_ABS64 .data+0x34
每种重定位类型均由一些参数定义:S是重定位所引用符号的地址(上面的VALUE),P是位置的地址(偏移量加上节本身的地址),A( for addenda)是汇编器保留的值。
连接成可执行函数,命令如下
ld -g -o print64-f print64-f.o mult_by_5.o -lc -I /lib64/ld-linux-aarch64.so.1
查看二进制文件
objdump -d print64-f
print64-f: file format elf64-littleaarch64
Disassembly of section .plt:
0000000000400290 <.plt>:
400290: a9bf7bf0 stp x16, x30, [sp,#-16]!
400294: f00000f0 adrp x16, 41f000
400298: f947fe11 ldr x17, [x16,#4088]
40029c: 913fe210 add x16, x16, #0xff8
4002a0: d61f0220 br x17
4002a4: d503201f nop
4002a8: d503201f nop
4002ac: d503201f nop
00000000004002b0 :
4002b0: 90000110 adrp x16, 420000
4002b4: f9400211 ldr x17, [x16]
4002b8: 91000210 add x16, x16, #0x0
4002bc: d61f0220 br x17
00000000004002c0 :
4002c0: 90000110 adrp x16, 420000
4002c4: f9400611 ldr x17, [x16,#8]
4002c8: 91002210 add x16, x16, #0x8
4002cc: d61f0220 br x17
Disassembly of section .text:
00000000004002d0 <_start>:
4002d0: 580001e0 ldr x0, 40030c
4002d4: 97fffffb bl 4002c0
4002d8: 58000220 ldr x0, 40031c
4002dc: 58000241 ldr x1, 400324
4002e0: 97fffff4 bl 4002b0
4002e4: 58000200 ldr x0, 400324
4002e8: f9400000 ldr x0, [x0]
4002ec: 94000012 bl 400334
4002f0: aa0003e2 mov x2, x0
4002f4: 58000181 ldr x1, 400324
4002f8: f9400021 ldr x1, [x1]
4002fc: 580000c0 ldr x0, 400314
400300: 97fffff0 bl 4002c0
400304: d2800ba8 mov x8, #0x5d // #93
400308: d4000001 svc #0x0
000000000040030c :
40030c: 00420010 .word 0x00420010
400310: 00000000 .word 0x00000000
0000000000400314 :
400314: 00420028 .word 0x00420028
400318: 00000000 .word 0x00000000
000000000040031c :
40031c: 0042003c .word 0x0042003c
400320: 00000000 .word 0x00000000
0000000000400324 :
400324: 00420040 .word 0x00420040
400328: 00000000 .word 0x00000000
000000000040032c :
40032c: 00420044 .word 0x00420044
400330: 00000000 .word 0x00000000
0000000000400334 :
400334: 8b000800 add x0, x0, x0, lsl #2
400338: d65f03c0 ret
地址也可以通过objdump -h(加上标志-w使其更易读)来确定。 一个文件可能包含许多部分。
$objdump -hw print64-f
print64-f: file format elf64-littleaarch64
Sections:
Idx Name Size VMA LMA File off Algn Flags
0 .interp 0000001d 0000000000400190 0000000000400190 00000190 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA
1 .hash 00000018 00000000004001b0 00000000004001b0 000001b0 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA
2 .dynsym 00000048 00000000004001c8 00000000004001c8 000001c8 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA
3 .dynstr 00000023 0000000000400210 0000000000400210 00000210 2**0 CONTENTS, ALLOC, LOAD, READONLY, DATA
4 .gnu.version 00000006 0000000000400234 0000000000400234 00000234 2**1 CONTENTS, ALLOC, LOAD, READONLY, DATA
5 .gnu.version_r 00000020 0000000000400240 0000000000400240 00000240 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA
6 .rela.plt 00000030 0000000000400260 0000000000400260 00000260 2**3 CONTENTS, ALLOC, LOAD, READONLY, DATA
7 .plt 00000040 0000000000400290 0000000000400290 00000290 2**4 CONTENTS, ALLOC, LOAD, READONLY, CODE
8 .text 0000006c 00000000004002d0 00000000004002d0 000002d0 2**2 CONTENTS, ALLOC, LOAD, READONLY, CODE
9 .dynamic 00000140 000000000041fea0 000000000041fea0 0000fea0 2**3 CONTENTS, ALLOC, LOAD, DATA
10 .got 00000008 000000000041ffe0 000000000041ffe0 0000ffe0 2**3 CONTENTS, ALLOC, LOAD, DATA
11 .got.plt 00000028 000000000041ffe8 000000000041ffe8 0000ffe8 2**3 CONTENTS, ALLOC, LOAD, DATA
12 .data 0000003c 0000000000420010 0000000000420010 00010010 2**2 CONTENTS, ALLOC, LOAD, DATA
13 .debug_aranges 00000060 0000000000000000 0000000000000000 00010050 2**4 CONTENTS, READONLY, DEBUGGING
14 .debug_info 00000094 0000000000000000 0000000000000000 000100b0 2**0 CONTENTS, READONLY, DEBUGGING
15 .debug_abbrev 00000028 0000000000000000 0000000000000000 00010144 2**0 CONTENTS, READONLY, DEBUGGING
16 .debug_line 0000008b 0000000000000000 0000000000000000 0001016c 2**0 CONTENTS, READONLY, DEBUGGING
VMA列定义该部分的地址。 在我们的例子中,.data位于0000000000420010 。我们的变量位于00420010、00420028….。可以看到这要求链接器打印生成的可执行文件的映射。
ld -g -o print64-f print64-f.o mult_by_5.o -lc -I /lib64/ld-linux-aarch64.so.1 --print-map
在执行程序之前,需要将其放入内存。 此过程称为加载。 通常,操作系统负责加载程序。
链接发生后,地址在最终程序文件中进行了硬编码。 在这种情况下,加载程序任务非常简单,只需将程序文件的相关位复制到内存中即可。 地址已经由链接器固定,因此只要我们将程序复制(即加载)到正确的内存地址中,就可以完成。
像Linux这样的现代操作系统,由于对此提供了特定的硬件支持,因此可以向进程(即正在运行的程序)提供所谓的虚拟内存。 虚拟内存给人一种幻觉,即进程可以根据需要使用内存空间。 此机制还提供隔离:一个进程无法写入另一个进程的内存。 运行几个要在同一地址加载的程序不是问题,因为它们只是在同一虚拟地址加载。 操作系统将这些虚拟地址映射到不同的物理地址。
位置无关代码(称为PIC)不使用绝对地址。而是使用某种机制在程序上建立相对地址。在运行时,可以通过一些额外的计算将这些相对地址转换为绝对地址。 ELF中使用的机制使用称为全局偏移表的表。该表包含条目,每个我们要访问的全局实体一个条目。在运行时,每个条目将保存对象的绝对地址。每个程序和动态库都有自己的GOT,不会与其他任何人共享。该GOT以一种无需使用绝对地址即可访问它的方式位于内存中。为此,必须使用相对于PC的地址。因此,GOT然后位于某个固定位置,可以在静态链接时间计算出该位置与参考它的距离。
实际上,仅动态库才需要位置无关的代码。 我们的主程序仍然可以使用非PIC访问,并且适用于库中的变量,链接器将处理这种情况。 但是没有什么可以阻止我们在主程序中使用PIC代码。 位置无关可执行文件(PIE)需要通过GOT进行所有访问。
我们不能使用一种机制来强制代码进行重定位(即,固定其地址)。 只能固定GOT(毕竟不是代码)。
静态库如下,保存一个变量
/* mylib.s */
.data
.balign 4
.globl myvar
myvar : .dword 42 /* 42 as initial value */
.size myvar, .-myvar
main.s文件如下:
/* main.s */
.data
.globl myvar
.text
.globl main
.balign 4
main:
ldr x0, addr_myvar /* r0 ← &myvar */
ldr x1, [x0] /* r1 ← *r0 */
add x1, x1, #1 /* r1 ← r1 + 1 */
str x1, [x0] /* *r0 ← r1 */
mov x0, #0
mov x8, 93
svc 0
addr_myvar: .dword myvar
进行编译,
as -o mylib.o mylib.s
编译静态库如下:
ar cru mylib.a mylib.o
as -o main.o main.s
gcc -o main main.o -L. -l:mylib.a
将mylib.s编程成动态库
as -o mylib.o mylib.s
gcc -shared -o mylib.so mylib.o -fpic
as -g -o main.o main.s
gcc -g -o main main.o -L. -l:mylib.so -Wl,-rpath,$(pwd)
myvar的地址将在GOT中的某些条目中。 确切地说,我们不知道链接器是哪一个。 但是,我们仍然需要首先获取GOT的基地址。
PIC访问将是相对于pc的。 鉴于程序和库将作为单个片段加载到内存中,我们可以要求静态链接器为我们提供GOT的确切偏移量。
在延迟加载下,我们第一次调用函数时必须加载它。 进一步的调用将使用以前加载的函数。 这是有效的,但需要更多的机制。 在ELF中,这是通过使用称为过程链接表(PLT)的附加表来实现的。
与GOT相比,PLT是代码。 PLT中的条目是很小的指令序列,仅分支到GOT中的条目。 功能的GOT条目由动态链接器初始化,并带有动态链接器内部功能的地址,该内部链接器检索功能的地址,使用该地址更新GOT并分支到该地址。 因为动态链接器更新了GOT表,所以通过PLT进行的下一次调用(该调用只是分支到GOT)将直接转到该函数。
为什么不直接调用GOT中的地址,或者为什么要使用PLT。 原因是动态链接器必须知道我们要第一次加载哪个函数,如果我们直接调用GOT中的地址,则需要设计一种机制来判断必须加载哪个函数。 一种方法是将函数的GOT条目初始化为准备所有内容的表,以便动态加载程序知道需要加载的确切函数。 但这实际上相当于PLT。
所有这一切看起来都过于复杂,但是好消息是,链接器创建了这些PLT条目,它们可以用作常规函数调用。 无需获取GOT条目的地址以及我们对变量所做的所有事情(如果要使用该函数的地址,我们仍然必须这样做!)。 我们总是可以这样做,但这会使代码膨胀,因为每个函数调用都需要在GOT表中进行复杂的索引。 该机制适用于PIC和非PIC,这就是我们能够调用诸如printf之类的C库函数的原因,而不必担心它是否来自动态库(并且除非我们使用-static生成一个 完全静态可执行文件)。 也就是说,我们可以显式使用后缀@PLT表示我们要通过PLT调用函数。 对于在库内进行的调用,这是必需的。