为 NEMU Bare-Metal 编程:编译、链接与加载; ELF; DEBUG Makefile;
资料
DEBUG Makefile
make -nB ARCH=x86_64-qemu
可以查看完整的编译、链接到 x86-64 的过程 (不实际进行编译), 我将此内容输出到了a.log
中
可能得到如下输出内容:
一坨,完全不想看...
但是我们可以想办法将这一坨尽量散开来易读点:
即然其参数单独起一行,并且带有缩进
方法:
- VIM 下按
V
进入VISUAL,选取需要展开的内容 - 然后按
:
,此时左下角会出现:'<',>
的内容- 这是 Vim 在可视模式(Visual)下回车后自动补的范围标记,表示“从选区的开始行 '< 到结束行 '>”都要执行某条命令
- 输入
s/ /\r /g
, 这时左下角看起来像是:'<',>s/ /\r /g
- s 就是 substitute(替换)。最后的 g 是 flag,表示在每一行中全局(global)替换所有匹配,而不是只替换每行的第一个。语法和sed一样。
- 所以整个语法表示将选中的内容中全部空格替换为换行符+空格
ELF 格式
ELF(Executable and Linking Format)是一种对象文件的格式,用于定义不同类型的对象文件(Object files)中都放了什么东西、以及都以什么样的格式去放这些东西。
- 段头部表其实是Programer Header Table
- 需要注意可重定位目标文件和可执行目标文件的ELF结构是不同的
- 可重定位目标文件比可执行目标文件的ELF多了.rel节,这是Relocation Entries(重定位条目),告诉链接器或 loader 在运行时/链接时如何修正代码或数据中的地址引用
- 在重定位完后生成的可执行目标文件自然可以抛弃掉.rel节了
- 可执行目标文件的ELF比可重定位目标文件多了Programer Header Table
- 程序头表的作用主要是分段,告知每个段(segment)的 类型,段的 文件大小、内存大小、标志(可读/写/可执行),在文件和内存中的 偏移、虚拟地址 (Vaddr)、物理地址 (Paddr);
- 即完全是为了将ELF文件加载进内存而服务的
段(Segment)和节(Section)
- Segment(段):运行时加载单元,由 Program Header Table 描述,把若干节映射到内存中。
- Section(节):逻辑上的分类,用来存放不同类型的数据或代码;由 Section Header Table 管理。
解析ELF格式
我们通过readelf
工具解析ELF文件
readelf -a
(或 readelf --all
)会一次性打印出 ELF 文件的几乎所有可用信息。其实际调用相当于组合了多个选项,通常包括:
-h // ELF Header
-l // Program Headers
-S // Section Headers
-s // Symbol tables (.symtab 和 .dynsym)
-r // Relocation entries
-d // Dynamic section
-D // 同上,取决于版本
-V // Version sections(.gnu.version, .gnu.version_r, .gnu.version_d)
-A // Architecture-specific flags
-n // Note sections
-G // .dynsym 哈希表
-hb // Build‑ID note(某些版本)
-p .<section> // Dump 字符串表、调试节等
下面以readelf -a main.o(重定位文件)
以及 read -a hello-x86_64-qemu.elf(可执行文件)
的输出为例
ELF Header(readelf -h
)
readelf -a main.o
ELF Header:
Magic: 7f 45 4c 46 02 01 01 00 00 00 00 00 00 00 00 00
Class: ELF64
Data: 2's complement, little endian
Version: 1 (current)
OS/ABI: UNIX - System V
ABI Version: 0
Type: REL (Relocatable file)
Machine: Advanced Micro Devices X86-64
Version: 0x1
Entry point address: 0x0
Start of program headers: 0 (bytes into file)
Start of section headers: 504 (bytes into file)
Flags: 0x0
Size of this header: 64 (bytes)
Size of program headers: 0 (bytes)
Number of program headers: 0
Size of section headers: 64 (bytes)
Number of section headers: 13
Section header string table index: 12
告知的重要信息有:
- 魔数 (ELF32 vs ELF64)
- 类型(可执行、可重定位、共享对象、核心转储)
- 机器架构
- 入口点地址(程序从何处开始执行)
- 程序头表偏移、节头表偏移、头大小、条目大小 & 数量 等
注意Number of section headers: 13
告知的是节条目数量,Number of program headers: 0
告知的是程序头表条目数量
Program Header Table(readelf -l
)
read -a hello-x86_64-qemu.elf
Program Headers:
Type Offset VirtAddr PhysAddr
FileSiz MemSiz Flags Align
LOAD 0x00000000000000b0 0x00000000001000b0 0x00000000001000b0
0x00000000000035f0 0x0000000000114c40 RWE 0x20
GNU_STACK 0x0000000000000000 0x0000000000000000 0x0000000000000000
0x0000000000000000 0x0000000000000000 RW 0x10
Section to Segment mapping:
Segment Sections...
00 .text .rodata .data .bss
01
- 每个段(segment)的 类型(LOAD、DYNAMIC、INTERP、NOTE…)
- 在ELF文件中的偏移和虚拟地址 (Vaddr)、物理地址 (Paddr)
- 段的 文件大小、内存大小、标志(可读/写/可执行)
- 对齐 要求
在程序头表中是以Segment
为单位,其中Segment‑to‑Section Mapping会显示哪个 section 落在哪个 segment 里(“Section to Segment mapping”),帮助理解段/节如何装载到内存。
节头表(readelf -S
)
read -a hello-x86_64-qemu.elf
Section Headers:
[Nr] Name Type Address Offset
Size EntSize Flags Link Info Align
[ 0] NULL 0000000000000000 00000000
0000000000000000 0000000000000000 0 0 0
[ 1] .text PROGBITS 00000000001000b0 000000b0
0000000000002864 0000000000000000 WAX 0 0 16
[ 2] .rodata PROGBITS 0000000000102920 00002920
0000000000000949 0000000000000000 A 0 0 32
[ 3] .data PROGBITS 0000000000103280 00003280
0000000000000420 0000000000000000 WA 0 0 32
[ 4] .bss NOBITS 00000000001036a0 000036a0
0000000000111650 0000000000000000 WA 0 0 32
[ 5] .comment PROGBITS 0000000000000000 000036a0
000000000000002b 0000000000000001 MS 0 0 1
[ 6] .symtab SYMTAB 0000000000000000 000036d0
0000000000000cf0 0000000000000018 7 57 8
[ 7] .strtab STRTAB 0000000000000000 000043c0
0000000000000574 0000000000000000 0 0 1
[ 8] .shstrtab STRTAB 0000000000000000 00004934
000000000000003d 0000000000000000 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),
D (mbind), l (large), p (processor specific)
- 列出所有节(section)的 名称、类型(PROGBITS、SYMTAB、STRTAB、REL/A、NOTE…)
- 每节的 地址(虚拟地址)、偏移(在ELF文件中的偏移)、大小、条目大小(对于表格类型)
- 标志(ALLOC、WRITE、EXECINSTR…)、链接关系(sh_link 指向哪个节)
- 对齐 和 条目大小(sh_entsize,如符号表中每条大小)
Relocation Entries 重定向条目节(readelf -r)与 符号表(readelf -s)
readelf -a main.o
,重定向条目节只出现在可重定位文件中
Relocation section '.rela.text.startup' at offset 0x148 contains 2 entries:
Offset Info Type Sym. Value Sym. Name + Addend
00000000000b 000200000002 R_X86_64_PC32 0000000000000000 .LC0 - 4
000000000010 000400000004 R_X86_64_PLT32 0000000000000000 say - 4
- Offset是针对
.text.startup
这一节开始的 - Info 通过划分高32位和低32位来表达信息:
- 高32位
0002
是 符号表索引(Sym. index = 2,指向符号表里的第三个条目)。 - 低32位
00000002
是重定位类型(Type = 2 → R_X86_64_PC32)
- 高32位
- Type有
R_X86_64_PC32
和R_X86_64_32
,不同Type在重定位时有不同的计算方式 - Addend是 重定位表达式中的常量部分,Sym.Name指示出了符号
在 GCC 生成的汇编中,像 .LC0、.LC1 这样的标签是 编译器自动生成的“文字常量(Literal Constant)”符号。具体含义如下:
- “LC” 表示 Literal Constant(文字常量)。
- GCC 用 .LC 前缀命名所有不需要用户命名的文字,比如字符串常量、浮点常量等。
- 数字(如 0, 1)是编号,在一个源文件中从 0 开始依次递增。
.LC0在这里出现是因为在源代码main.c中并没有显式地给 "hello\n" 命名,为了生成能引用的机器码,编译器就会为它创建一个唯一的名字(比如 .LC0)。
其符号表部分:
Symbol table '.symtab' contains 5 entries:
Num: Value Size Type Bind Vis Ndx Name
0: 0000000000000000 0 NOTYPE LOCAL DEFAULT UND
1: 0000000000000000 0 FILE LOCAL DEFAULT ABS main.c
2: 0000000000000000 0 NOTYPE LOCAL DEFAULT 4 .LC0
3: 0000000000000000 27 FUNC GLOBAL HIDDEN 5 main
4: 0000000000000000 0 NOTYPE GLOBAL DEFAULT UND say
- Value表示符号的地址
- 对于可重定位的模块来说,value 是该符号相对于它所在节(section)起始处的偏移。
- 对于可执行目标文件来说,该值是一个绝对运行时地址。
- Ndx刚好告知了所属节索引,例如Ndx=1 表示 .text 节,而 Ndx=3 表示 .data 节。
- ABS 代表不该被重定位的符号;
- UNDEF 代表未定义的符号,也就是在本目标模块中引用,但是却在其他地方定义的符号;
- COMMON 表示还未被分配位置的未初始化的数据目标。
Bare-Metal 编程:编译、链接与加载
# main.c
void say(const char *s);
int main() {
say("hello\n");
}
# say.c
void putch(char ch);
int putchar(int ch);
void say(const char *s) {
for (; *s; s++) {
#ifdef __ARCH__
putch(*s); // AbstractMachine,没有 libc,调用 TRM API 打印字符
#else
putchar(*s); // 操作系统,调用 libc 打印字符
#endif
}
}
编译
我们能够直接通过如下命令进行编译其不会报错
gcc -c -O2 -o main.o main.c
gcc -c -O2 -o say.o say.c
因为此时生成的还只是可重定位目标文件,这时对于未实现的函数(如main.c中的say, say.c中的putch和putchar)都只是将其作为未定义的符号。
在链接过程中生成可执行目标文件的过程中若还是没有找到符号的定义实现那么就会报错
需要重定向的符号
我们通过objdump -d
来反汇编.text
节内容
# objdump -d main.o
main.o: file format elf64-x86-64
Disassembly of section .text.startup:
0000000000000000 <main>:
0: f3 0f 1e fa endbr64
4: 48 83 ec 08 sub $0x8,%rsp
8: 48 8d 3d 00 00 00 00 lea 0x0(%rip),%rdi # f <main+0xf>
f: e8 00 00 00 00 call 14 <main+0x14>
14: 31 c0 xor %eax,%eax
16: 48 83 c4 08 add $0x8,%rsp
1a: c3 ret
- x86-64 Linux 调用约定下,第一个参数放
%rdi
lea 0x0(%rip),%rdi
应该是想将hello\n
这个字符串的地址放到%rdi
中- RIP 是 x86-64 架构中的指令指针寄存器,主要用途为:
- 指向下一条要执行的指令
- RIP 相对寻址
- x86-64 引入了基于 RIP 的相对寻址模式:许多指令允许对操作数使用形如 disp(%rip) 的寻址方式。
- 汇编如 lea label(%rip), %rdi、mov (%rip+offset), %rax 等,会在编码时留下一个相对偏移,链接或运行时再计算为绝对地址。
但是可以看到我们基于%rip
的偏移为0x0
,这是因为hello\n
这个字符串的位置还有待重定向,目前不知道hello\n
这个字符串的具体位置,所以先将偏移设置成为0x0
因为我们将hello\n
这个字符串的偏移设置成为0x0
,所以0x0(%rip)
的值计算出来就为f
,即main+0xf
,所以我们能够看到指令后面的注释为# f <main+0xf>
同理对于指令f: e8 00 00 00 00 call 14 <main+0x14>
- 机器码 e8 imm32 表示 PC-relative call,即跳转到当前下一地址加上 imm32 偏移处执行
- 我们本来想要call的是say函数,但是say 符号还有待重定向,目前不知道say函数的具体地址,所以这里的imm32也先设置为
0x0
,所以当前下一地址(14)+ 0x0 = 14
反汇编say.o依旧能够看到putchar符号和s符号还有待重定向
重定向在链接阶段得以执行
链接
链接命令如下:
x86_64-linux-gnu-ld
-z
noexecstack
-melf_x86_64
-N
-Ttext-segment=0x00100000
-o
/home/cilinmengye/tmp/bare-metal/build/hello-x86_64-qemu.elf
--start-group
/home/cilinmengye/tmp/bare-metal/build/x86_64-qemu/main.o
/home/cilinmengye/tmp/bare-metal/build/x86_64-qemu/say.o
/home/cilinmengye/ics2023/abstract-machine/am/build/am-x86_64-qemu.a
/home/cilinmengye/ics2023/abstract-machine/klib/build/klib-x86_64-qemu.a
--end-group
只链接了 main.o, say.o 和必要的库函数 (AbstractMachine 和 klib;在这个例子中,我们甚至可以不链接 klib 也能正常运行)。使用的链接选项:
选项 | 含义 |
---|---|
-z noexecstack |
在最终 ELF 中标记 不需要可执行的用户栈(NX bit on stack)。 可防止栈区域被当作可执行内存,提升安全性。 |
-m elf_x86_64 |
指定使用 ELF x86_64 的目标文件格式和链接脚本仿真器(emulation)。通常在交叉或多目标链路中显式指定,告诉 ld 按 x86_64 的 ABI 和段布局来生成输出。 |
-N (等同于 --omagic ) |
生成一个 “omagic” 可执行: 1. 使 .text + .data 段连在一起,且均为可读写;2. 不对段进行页面对齐(page alignment)。 主要用于嵌入式、裸机或特殊镜像。 |
-Ttext-segment=0x00100000 |
强制将 .text 段(代码段) 放在 虚拟地址 0x0010_0000 上。适合你要在 QEMU 或裸机环境中,将程序加载到该固定地址执行。 |
-o <path> |
指定 输出文件 的路径,这里是生成 /home/.../hello-x86_64-qemu.elf 。 |
上述我们是通过链接了自己编写的库函数,若想要依赖本地Linux的环境进行最小的链接可以使用如下命令:
ld -dynamic-linker /lib64/ld-linux-x86-64.so.2 \
/usr/lib/x86_64-linux-gnu/crt1.o \
/usr/lib/x86_64-linux-gnu/crti.o \
main.o say.o -lc \
/usr/lib/x86_64-linux-gnu/crtn.o
- 在程序头部表的引导下,加载器将可执行文件的片(chunk)复制到代码段和数据段。接下来,加载器跳转到程序的入口点,也就是 _start函数的地址。
- 这个函数是在系统目标文件 ctrl.o 中定义的,对所有的 C 程序都是一样的。
- _start 函数调用系统启动函数 __libc_start_main,该函数定义在 libc.so 中。
- 它初始化执行环境,调用用户层的 main 函数,处理 main 函数的返回值,并且在需要的时候把控制返回给内核。
- 前缀 crt 代表 “C runtime”(C 运行时)或 “C runtime startup/termination” 相关内容。
- 后缀:
- crt1.o:通常包含程序入口 _start 以及调用 main 之前的最低限度启动代码。
- crti.o:C runtime initialization 部分,通常定义 .init 段的起始符号,或放置初始化函数序列的前导。
- crtn.o:C runtime termination 部分,通常定义 .fini 段的结束符号,或放置终结函数序列的尾部。
它们按字母或数字区分用途:1 表示最早、主要的启动对象;i(init)表示初始化相关;n(near end/termination)表示终结相关。
/lib64/ld-linux-x86-64.so.2 是 Linux x86_64 平台上动态链接器(dynamic linker,也称 runtime loader)的 ELF 共享对象, 负责动态链接库的加载,没有它就无法加载动态链接库 (libc)。
“.so” 表明这是一个共享对象;“.2”通常是 SONAME 版本号,用以处理与 libc 等库的 ABI 兼容
话又说回来,在我们自己编写的AM中,_start
函数在/home/cilinmengye/ics2023/abstract-machine/am/src/x86/qemu/start64.S
下实现
其中的函数调用关系为:
_start-->.long_mode_init-->_start64-->_start_c
_start_c
在/home/cilinmengye/ics2023/abstract-machine/am/src/x86/qemu/trm.c
中实现
static inline void stack_switch_call(void *sp, void *entry, uintptr_t arg) {
asm volatile (
#if __x86_64__
"movq %0, %%rsp; movq %2, %%rdi; jmp *%1" : : "b"((uintptr_t)sp), "d"(entry), "a"(arg)
#else
"movl %0, %%esp; movl %2, 4(%0); jmp *%1" : : "b"((uintptr_t)sp - 8), "d"(entry), "a"(arg)
#endif
);
}
void _start_c(char *args) {
if (boot_record()->is_ap) {
__am_othercpu_entry();
} else {
__am_bootcpu_init();
stack_switch_call(stack_top(&CPU->stack), call_main, (uintptr_t)args);
}
}
"b"、"d"、"a" 是 GCC 规定的寄存器约束:
- "a" → %rax,
- "b" → %rbx,
- "d" → %rdx。
这段 GCC 内联汇编的写法遵循下面的模板:
asm volatile (
"template–instructions"
: /* outputs */
: /* inputs */
: /* clobbers */
);
具体到代码为:
asm volatile (
"movq %0, %%rsp; " // ①
"movq %2, %%rdi; " // ②
"jmp *%1" // ③
: /* —— 没有“输出”操作数 */
: "b"((uintptr_t)sp), /* 输入操作数 0 */
"d"(entry), /* 输入操作数 1 */
"a"(arg) /* 输入操作数 2 */
/* —— 没有“破坏”寄存器列表 */
);
模板字符串与 % 占位,模板里出现的 %0, %1, %2 分别对应下面输入列表中的第 0、1、2 个操作数。GCC 会把这些 %n 替换成满足约束的寄存器或内存引用:
- %0 → "b"((uintptr_t)sp)
- %1 → "d"(entry)
- %2 → "a"(arg)
所以上述代码做的事件就是:把操作数 0(sp 的值)写到 %rsp 寄存器,完成“切栈”。把操作数 2(arg)写到第一个函数参数寄存器 %rdi,准备给下一步跳转的函数传参。间接无返回跳到操作数 1(entry)指向的地址,开始执行新栈上的目标函数。
一些参数注意点:
- _start 里:movq $0x10000, %rdi → _start_c(char * args) 中的 args = (char*)0x10000。
- _start_c 再将 args (0x10000) 传给 stack_switch_call,最终到 call_main(const char * args) 中,此时 args 依然是 0x10000。
- (uintptr_t)args 保证“指针→整数”转换在所有平台都安全,并满足内联汇编对整数输入寄存器的要求。
重定向
我们再来看看重定向后的main.c的汇编代:
00000000001000b0 <main>:
1000b0: f3 0f 1e fa endbr64
1000b4: 48 83 ec 08 sub $0x8,%rsp
1000b8: 48 8d 3d 61 28 00 00 lea 0x2861(%rip),%rdi # 102920 <__am_irqall+0x16>
1000bf: e8 0c 00 00 00 call 1000d0 <say>
1000c4: 31 c0 xor %eax,%eax
1000c6: 48 83 c4 08 add $0x8,%rsp
1000ca: c3 ret
1000cb: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)
关注如下两行 1000b8: 48 8d 3d 61 28 00 00 lea 0x2861(%rip),%rdi # 102920 <__am_irqall+0x16>
1000bf: e8 0c 00 00 00 call 1000d0 <say>
可以发现lea后的0x0被替换成了0x2861,正好0x2861 + 1000bf == 102920 == __am_irqall+0x16
通过ELF重定位条目表可以知道字符串hello\n
为 R_X86_64_PC32 类型,即使用 PC 相对地址进行寻址,一个 PC 相对地址就是距程序计数器(PC)的当前运行时值的偏移量。
0x2861
的计算依据公式, 假设此时链接器已经为.LC0即hello\n
符号选择了运行时地址ADDR(.LC0),为节.text选择了运行时地址ADDR(.text),那么计算方法为:0x2861 = ADDR(.LC0) + 对应重定位条目中的addend - ADDR(.text) - 对于重定位条目中的Offset
CPU Reset
在 x86 架构上,CPU 在上电或复位(reset)之后,最先进入的就是 实模式(Real Mode),而 BIOS 就是在这个模式下工作的。这就是所谓的 Real‑mode BIOS boot, 其表现为:
-
运行在 16-bit 模式 (现在 CPU 的行为就像 8086): 16 位寻址,实模式下,CPU 以 16 位寄存器为主,最大只能直接寻址 1 MiB(段基址 × 16 + 偏移,20 位地址线)。
-
分段机制: 内存访问通过 段寄存器:偏移 组合,比如
0x07C0:0x0000
就对应物理地址0x7C00
。 -
无限制访问: 所有代码/数据都在一个平坦空间里,没有保护和权限检查。任何指令都可以访问任意物理地址。
-
BIOS 运行环境: BIOS 固件(包括 POST、硬件检测、引导扇区加载等)都是用这种模式编写的,用 16 位汇编或 C 语言编译的 16 位代码。
-
引导扇区执行: BIOS 固件会依次扫描系统中的存储设备 (磁盘、优盘等,Boot Order 通常可以设置),然后将第一个可启动磁盘的前 512 字节 (主引导扇区, Master Boot Record, MBR) 加载到物理内存的 0x7c00 地址,然后跳转到该地址执行。此时仍然处在实模式。
第一个可启动磁盘的前 512 字节代码即是我们的bootloader程序,该程序末尾应该以0x55AA
这4个字节进行结尾。然后其通常负责进入保护模式(Protected Mode)或长模式(Long Mode)并加载更高级的内核。
-
保护模式(Protected Mode)
- 引入 段保护、分页、权限级别 (Ring 0–3);真正实现多任务和现代操作系统所需的内存保护。
-
长模式(Long Mode)
- AMD/Intel 在 x86-64 扩展中引入的 64 位模式,基于保护模式之上,支持 64 位指令集、更多通用寄存器、更大虚拟地址空间。
这里,我们的bootloader程序具体实现在/home/cilinmengye/ics2023/abstract-machine/am/src/x86/qemu/boot
目录下
NEMU MakeFile 整个运行过程
补充知识
objcopy
riscv64-linux-gnu-objcopy -S --set-section-flags .bss=alloc,contents -O binary /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
这个命令的作用是把一个 ELF 格式的可执行文件转换成「原始二进制」格式(flat binary),并且确保其中的 .bss
段也被展开成实际的零字节数据,最后去掉所有符号表和调试信息。具体来说:
-
-S
去除所有符号表和调试信息(strip),让输出更精简。 -
--set-section-flags .bss=alloc,contents
默认情况下,ELF 中的.bss
段是 “NOBITS” 类型,也就是不占文件空间,只在加载时由运行时环境清零分配。
这条选项把.bss
段的属性改成alloc
:保留在加载映像中contents
:当作有数据段来处理
这样 objcopy 在生成二进制时,就会把.bss
段对应的“零填充”字节写入到输出文件里。
-
-O binary
指定输出格式为 raw binary,也就是一个扁平的、不含任何 ELF 头或节表的裸数据文件。 -
最后两个参数
/home/cilinmengye/.../amtest-riscv32-nemu.elf /home/cilinmengye/.../amtest-riscv32-nemu.bin
分别是输入的 ELF 文件路径和生成的
.bin
文件路径。
这条命令把 amtest-riscv32-nemu.elf
转成一个扁平的 .bin
镜像,去掉所有符号,并且把 .bss
段中的零初始化区也写入到二进制里,得到一个可直接烧录或加载到模拟环境/裸机上的镜像文件。
ld链接脚本
参考博客:
riscv64-linux-gnu-ld
-z
noexecstack
-melf64lriscv
-T
/home/cilinmengye/ics2023/abstract-machine/scripts/linker.ld
--defsym=_pmem_start=0x80000000
--defsym=_entry_offset=0x0
--gc-sections
-e
_start
-melf32lriscv
-o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf
--start-group
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/main.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/rtc.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/mp.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/hello.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/keyboard.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/intr.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/video.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/vm.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/audio/audio-data.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/devscan.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/audio.o
/home/cilinmengye/ics2023/abstract-machine/am/build/am-riscv32-nemu.a
/home/cilinmengye/ics2023/abstract-machine/klib/build/klib-riscv32-nemu.a
--end-group
在这条链接命令中,-T /home/cilinmengye/ics2023/abstract-machine/scripts/linker.ld 的作用,就是告诉 GNU 链接器(riscv64‑linux‑gnu‑ld)“请使用我自己写的这个链接脚本”,而不是默认脚本。
在开发裸机代码时,必须指定代码入口点的位置以及全局静态变量、堆栈以及可能的堆的位置。无需使用操作系统或操作系统内存模型。链接器脚本定义了输出映像中要使用的裸机代码内存模型的基础。
我们具体的链接脚本为:
ENTRY(_start)
PHDRS { text PT_LOAD; data PT_LOAD; }
SECTIONS {
/* _pmem_start and _entry_offset are defined in LDFLAGS */
. = _pmem_start + _entry_offset;
.text : {
*(entry)
*(.text*)
} : text
etext = .;
_etext = .;
.rodata : {
*(.rodata*)
}
.data : {
*(.data)
} : data
edata = .;
_data = .;
.bss : {
_bss_start = .;
*(.bss*)
*(.sbss*)
*(.scommon)
}
_stack_top = ALIGN(0x1000);
. = _stack_top + 0x8000;
_stack_pointer = .;
end = .;
_end = .;
_heap_start = ALIGN(0x1000);
}
ENTRY(_start)
: 明确告诉链接器从哪个符号开始执行.text : { *(entry) *(.text*) } : text
- 先把所有来自输入文件里名字为 entry 的节(section)放进输出的 .text
- 然后才放入所有名字以 .text 开头的段((.text))
正好我们是有.entry节的,其被链接到了am-riscv32-nemu.a
中,源文件是/home/cilinmengye/ics2023/abstract-machine/am/src/riscv/nemu/start.S
.section entry, "ax"
.globl _start
.type _start, @function
_start:
mv s0, zero
la sp, _stack_pointer
jal _trm_init
-
.section entry, "ax"
-
作用:切换到(或新建)一个名为
.entry
的节(section),后面的机器码都会放到这里。 -
FLAGS
"ax"
:这是这个节的属性标志:a
(alloc) —— 可分配,表示链接生成的 ELF 文件里要为它分配空间(否则类似.note
之类的节可能不会占用输出文件空间/映像空间)。x
(execute) —— 可执行,告诉链接器和加载器,这里是可执行代码,不要把它当数据来对待。
-
用途:把启动代码放在一个自定义节里,便于 linker script 用
*(entry)
精确地把它排到最前面。
-
-
.globl _start
-
作用:把符号
_start
声明为“全局”(global),也就是对外可见。 -
意义:
- 链接器才能找到并把它放进输出文件的符号表中;
- 链接脚本中
ENTRY(_start)
或命令行-e _start
才能定位到它; - 最终 ELF Header 的
e_entry
(入口地址)才会指向这个符号。
-
-
.type _start, @function
-
作用:在符号表里标记
_start
的类型是一个函数(function)。 -
意义:
- 帮助调试器和反汇编工具(如
objdump -t
、readelf -s
)正确识别它是一段可执行代码入口; - 虽然对裸机启动并非绝对必要,但这是一个好习惯,让符号表更准确、可读性更好。
- 帮助调试器和反汇编工具(如
-
上述操作保证了在执行此程序时,一定是Start.S中的_start代码先被执行
同时像我这个ld脚本,定义在其中的变量如 end
, _heap_start
, _stack_top
,最终可以在c程序中被用到, 因为在链接脚本中定义了符号(symbol),它们就会出现在最终的 ELF 符号表里,并且可以在 C 代码中通过 extern 声明来引用它们。
NEMU
/home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter --log=/home/cilinmengye/ics2023/nemu/build/nemu-log.txt
在nemu目录下通过make -nB ARCH=$ISA-nemu run
发现内容就是将目录下全部.c文件编译成.o,然后最终将这些.o文件链接成成/home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter可执行文件
那么如何将基于NEMU运行的镜像加载到NEMU上?
make
-C
/home/cilinmengye/ics2023/nemu
ISA=riscv32
run
ARGS="-l
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/nemu-log.txt
-b
-e
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf"
IMG=/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
-------------------------------------------------------
/home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter
-l
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/nemu-log.txt
-b
-e
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
---------------------------------------------------------
NEMU_EXEC := $(BINARY) $(ARGS) $(IMG)
run: run-env
$(call git_commit, "run NEMU")
$(NEMU_EXEC)
来看看源代码部分:
static char *elf_file = NULL;
static char *img_file = NULL;
static void restart() {
/* Set the initial program counter. */
cpu.pc = RESET_VECTOR;
/* The zero register is always 0. */
cpu.gpr[0] = 0;
}
void init_isa() {
/* Load built-in image. */
memcpy(guest_to_host(RESET_VECTOR), img, sizeof(img));
/* Initialize this virtual computer system. */
restart();
}
static long load_img() {
if (img_file == NULL) {
Log("No image is given. Use the default build-in image.");
return 4096; // built-in image size
}
FILE *fp = fopen(img_file, "rb");
Assert(fp, "Can not open '%s'", img_file);
fseek(fp, 0, SEEK_END);
long size = ftell(fp);
Log("The image is %s, size = %ld", img_file, size);
fseek(fp, 0, SEEK_SET);
int ret = fread(guest_to_host(RESET_VECTOR), size, 1, fp);
assert(ret == 1);
fclose(fp);
return size;
}
static int parse_args(int argc, char *argv[]) {
const struct option table[] = {
{"batch" , no_argument , NULL, 'b'},
{"log" , required_argument, NULL, 'l'},
{"diff" , required_argument, NULL, 'd'},
{"port" , required_argument, NULL, 'p'},
{"help" , no_argument , NULL, 'h'},
{"elf" , required_argument, NULL, 'e'},
{0 , 0 , NULL, 0 },
};
int o;
while ( (o = getopt_long(argc, argv, "-bhl:d:p:e:", table, NULL)) != -1) {
switch (o) {
case 'b': sdb_set_batch_mode(); break;
case 'p': sscanf(optarg, "%d", &difftest_port); break;
case 'l': log_file = optarg; break;
case 'd': diff_so_file = optarg; break;
case 'e': elf_file = optarg; break;
case 1: img_file = optarg; return 0;
default:
printf("Usage: %s [OPTION...] IMAGE [args]\n\n", argv[0]);
printf("\t-b,--batch run with batch mode\n");
printf("\t-l,--log=FILE output log to FILE\n");
printf("\t-d,--diff=REF_SO run DiffTest with reference REF_SO\n");
printf("\t-p,--port=PORT run DiffTest with port PORT\n");
printf("\t-e,--elf=FILE parse elf from FILE\n");
printf("\n");
exit(0);
}
}
return 0;
}
void init_monitor(int argc, char *argv[]) {
/* Perform some global initialization. */
/* Parse arguments. */
parse_args(argc, argv);
/* Perform ISA dependent initialization. */
init_isa();
/* Load the image to memory. This will overwrite the built-in image. */
long img_size = load_img();
...
}
所以我们使用-e 传递了elf文件路径,后接了镜像文件路径
在nemu启动时调用init_monitor函数-->parse_args解析参数,对img_file赋参-->init_isa函数,让CPU的PC指向存放镜像的内存地址-->load_img函数,让存放镜像的内存地址中保存我们的镜像数据
AM 镜像的生成
nemu我们就可以将其当做裸机看待
依据讲义的指导运行第一个C程序,我在am-kernels项目下执行make -nB ARCH=$ISA-nemu ALL=dummy run
其中所做的内容会将am-kernels下的.c文件编译成.o文件
然后将AM中的AM库和klib库编译成静态库
最终依据自己编写的ld链接脚本链接am-kernels下的.o文件,am和klib静态库,形成amtest-riscv32-nemu.elf可执行文件
riscv64-linux-gnu-ld
-z
noexecstack
-melf64lriscv
-T
/home/cilinmengye/ics2023/abstract-machine/scripts/linker.ld
--defsym=_pmem_start=0x80000000
--defsym=_entry_offset=0x0
--gc-sections
-e
_start
-melf32lriscv
-o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf
--start-group
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/main.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/rtc.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/mp.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/hello.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/keyboard.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/intr.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/video.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/vm.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/audio/audio-data.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/devscan.o
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/riscv32-nemu/src/tests/audio.o
/home/cilinmengye/ics2023/abstract-machine/am/build/am-riscv32-nemu.a
/home/cilinmengye/ics2023/abstract-machine/klib/build/klib-riscv32-nemu.a
--end-group
然后通过objcopy
工具形成镜像文件amtest-riscv32-nemu.bin
riscv64-linux-gnu-objcopy -S --set-section-flags .bss=alloc,contents -O binary /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf /home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
最终执行, 会将nemu中的.c文件编译成.o文件,然后链接成可执行文件,正如上面提到过的
make
-C
/home/cilinmengye/ics2023/nemu
ISA=riscv32
run
ARGS="-l
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/nemu-log.txt
-b
-e
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf"
IMG=/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
最后得到的执行命令为:
/home/cilinmengye/ics2023/nemu/build/riscv32-nemu-interpreter
-l
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/nemu-log.txt
-b
-e
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.elf
/home/cilinmengye/ics2023/am-kernels/tests/am-tests/build/amtest-riscv32-nemu.bin
解析$(AM_HOME)/Makefile
$(AM_HOME)/Makefile是整个系统编译的主要关键 其中有编译链接出库函数am.a和klib.a的archive标签,也有编译链接出ELF文件riscv-nemu.elf的文件:
archive: $(ARCHIVE)
$(ARCHIVE): $(OBJS)
@echo + AR "->" $(shell realpath $@ --relative-to .)
@$(AR) rcs $(ARCHIVE) $(OBJS)
$(IMAGE).elf: $(OBJS) $(LIBS)
@echo + LD "->" $(IMAGE_REL).elf
@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
以在/home/cilinmengye/ics2023/am-kernels/tests/am-tests
目录下执行make ARCH=$ISA-nemu ALL=dummy run
为例;
NAME = amtest
SRCS = $(shell find src/ -name "*.[cS]")
include $(AM_HOME)/Makefile
- 在
include $(AM_HOME)/Makefile
中$OBJS
依赖于SRCS
得出 run
标签实际上通过include $(AM_HOME)/scripts/$(ARCH).mk-->include $(AM_HOME)/scripts/platform/nemu.mk
最终引入:
run: image
$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin
而我们的image
标签有如下依赖:
image: $(IMAGE).elf
@$(OBJDUMP) -d $(IMAGE).elf > $(IMAGE).txt
@echo + OBJCOPY "->" $(IMAGE_REL).bin
@$(OBJCOPY) -S --set-section-flags .bss=alloc,contents -O binary $(IMAGE).elf $(IMAGE).bin
image: image-dep
image-dep: $(OBJS) $(LIBS)
@echo \# Creating image [$(ARCH)]
$(IMAGE).elf
依赖:
$(IMAGE).elf: $(OBJS) $(LIBS)
@echo + LD "->" $(IMAGE_REL).elf
@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
$(LIBS)
依赖AM下的am,klib库,所以引发了这两个库函数下的编译链接:
$(LIBS): %:
@$(MAKE) -s -C $(AM_HOME)/$* archive
其实@$(MAKE) -s -C $(AM_HOME)/$* archive
核心还是执行$(AM_HOME)/Makefile
中的archive
而archive
只依赖 $(OBJS)
,所以不会引发无限嵌套的情况
$(LINKAGE)
实际上就是$(OBJS)
和$(LIBS)
的内容
$(AM_HOME)/Makefile
最终全部展开为:
# Makefile for AbstractMachine Kernels and Libraries
### *Get a more readable version of this Makefile* by `make html` (requires python-markdown)
html:
cat Makefile | sed 's/^\([^#]\)/ \1/g' | markdown_py > Makefile.html
.PHONY: html
## 1. Basic Setup and Checks
### Default to create a bare-metal kernel image
ifeq ($(MAKECMDGOALS),)
MAKECMDGOALS = image
.DEFAULT_GOAL = image
endif
### Override checks when `make clean/clean-all/html`
ifeq ($(findstring $(MAKECMDGOALS),clean|clean-all|html),)
### Print build info message
$(info # Building $(NAME)-$(MAKECMDGOALS) [$(ARCH)])
### Check: environment variable `$AM_HOME` looks sane
ifeq ($(wildcard $(AM_HOME)/am/include/am.h),)
$(error $$AM_HOME must be an AbstractMachine repo)
endif
### Check: environment variable `$ARCH` must be in the supported list
ARCHS = $(basename $(notdir $(shell ls $(AM_HOME)/scripts/*.mk)))
ifeq ($(filter $(ARCHS), $(ARCH)), )
$(error Expected $$ARCH in {$(ARCHS)}, Got "$(ARCH)")
endif
### Extract instruction set architecture (`ISA`) and platform from `$ARCH`. Example: `ARCH=x86_64-qemu -> ISA=x86_64; PLATFORM=qemu`
ARCH_SPLIT = $(subst -, ,$(ARCH))
ISA = $(word 1,$(ARCH_SPLIT))
PLATFORM = $(word 2,$(ARCH_SPLIT))
### Check if there is something to build
ifeq ($(flavor SRCS), undefined)
$(error Nothing to build)
endif
### Checks end here
endif
## 2. General Compilation Targets
### Create the destination directory (`build/$ARCH`)
WORK_DIR = $(shell pwd)
DST_DIR = $(WORK_DIR)/build/$(ARCH)
$(shell mkdir -p $(DST_DIR))
### Compilation targets (a binary image or archive)
IMAGE_REL = build/$(NAME)-$(ARCH)
IMAGE = $(abspath $(IMAGE_REL))
ARCHIVE = $(WORK_DIR)/build/$(NAME)-$(ARCH).a
### Collect the files to be linked: object files (`.o`) and libraries (`.a`)
OBJS = $(addprefix $(DST_DIR)/, $(addsuffix .o, $(basename $(SRCS))))
LIBS := $(sort $(LIBS) am klib) # lazy evaluation ("=") causes infinite recursions
LINKAGE = $(OBJS) \
$(addsuffix -$(ARCH).a, $(join \
$(addsuffix /build/, $(addprefix $(AM_HOME)/, $(LIBS))), \
$(LIBS) ))
## 3. General Compilation Flags
### (Cross) compilers, e.g., mips-linux-gnu-g++
AS = $(CROSS_COMPILE)gcc
CC = $(CROSS_COMPILE)gcc
CXX = $(CROSS_COMPILE)g++
LD = $(CROSS_COMPILE)ld
AR = $(CROSS_COMPILE)ar
OBJDUMP = $(CROSS_COMPILE)objdump
OBJCOPY = $(CROSS_COMPILE)objcopy
READELF = $(CROSS_COMPILE)readelf
### Compilation flags
INC_PATH += $(WORK_DIR)/include $(addsuffix /include/, $(addprefix $(AM_HOME)/, $(LIBS)))
INCFLAGS += $(addprefix -I, $(INC_PATH))
ARCH_H := arch/$(ARCH).h
CFLAGS += -O2 -MMD -Wall -Werror $(INCFLAGS) \
-D__ISA__=\"$(ISA)\" -D__ISA_$(shell echo $(ISA) | tr a-z A-Z)__ \
-D__ARCH__=$(ARCH) -D__ARCH_$(shell echo $(ARCH) | tr a-z A-Z | tr - _) \
-D__PLATFORM__=$(PLATFORM) -D__PLATFORM_$(shell echo $(PLATFORM) | tr a-z A-Z | tr - _) \
-DARCH_H=\"$(ARCH_H)\" \
-fno-asynchronous-unwind-tables -fno-builtin -fno-stack-protector \
-Wno-main -U_FORTIFY_SOURCE -fvisibility=hidden
CXXFLAGS += $(CFLAGS) -ffreestanding -fno-rtti -fno-exceptions
ASFLAGS += -MMD $(INCFLAGS)
LDFLAGS += -z noexecstack
## 4. Arch-Specific Configurations
### Paste in arch-specific configurations (e.g., from `scripts/x86_64-qemu.mk`)
# 人为注释,用于展示下方真正引入的内容:-include $(AM_HOME)/scripts/$(ARCH).mk
# 人为注释,用于展示下方真正引入的内容:include $(AM_HOME)/scripts/isa/riscv.mk
CROSS_COMPILE := riscv64-linux-gnu-
COMMON_CFLAGS := -fno-pic -march=rv64g -mcmodel=medany -mstrict-align
CFLAGS += $(COMMON_CFLAGS) -static
ASFLAGS += $(COMMON_CFLAGS) -O0
LDFLAGS += -melf64lriscv
# overwrite ARCH_H defined in $(AM_HOME)/Makefile
ARCH_H := arch/riscv.h
# 人为注释,用于展示下方真正引入的内容:include $(AM_HOME)/scripts/platform/nemu.mk
AM_SRCS := platform/nemu/trm.c \
platform/nemu/ioe/ioe.c \
platform/nemu/ioe/timer.c \
platform/nemu/ioe/input.c \
platform/nemu/ioe/gpu.c \
platform/nemu/ioe/audio.c \
platform/nemu/ioe/disk.c \
platform/nemu/mpe.c
CFLAGS += -fdata-sections -ffunction-sections
LDFLAGS += -T $(AM_HOME)/scripts/linker.ld \
--defsym=_pmem_start=0x80000000 --defsym=_entry_offset=0x0
LDFLAGS += --gc-sections -e _start
NEMUFLAGS += -l $(shell dirname $(IMAGE).elf)/nemu-log.txt
NEMUFLAGS += -b
NEMUFLAGS += -e $(IMAGE).elf
CFLAGS += -DMAINARGS=\"$(mainargs)\"
CFLAGS += -I$(AM_HOME)/am/src/platform/nemu/include
.PHONY: $(AM_HOME)/am/src/platform/nemu/trm.c
image: $(IMAGE).elf
@$(OBJDUMP) -d $(IMAGE).elf > $(IMAGE).txt
@echo + OBJCOPY "->" $(IMAGE_REL).bin
@$(OBJCOPY) -S --set-section-flags .bss=alloc,contents -O binary $(IMAGE).elf $(IMAGE).bin
run: image
$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) run ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin
gdb: image
$(MAKE) -C $(NEMU_HOME) ISA=$(ISA) gdb ARGS="$(NEMUFLAGS)" IMG=$(IMAGE).bin
CFLAGS += -DISA_H=\"riscv/riscv.h\"
COMMON_CFLAGS += -march=rv32im_zicsr -mabi=ilp32 # overwrite
LDFLAGS += -melf32lriscv # overwrite
AM_SRCS += riscv/nemu/start.S \
riscv/nemu/cte.c \
riscv/nemu/trap.S \
riscv/nemu/vme.c
### Fall back to native gcc/binutils if there is no cross compiler
ifeq ($(wildcard $(shell which $(CC))),)
$(info # $(CC) not found; fall back to default gcc and binutils)
CROSS_COMPILE :=
endif
## 5. Compilation Rules
### Rule (compile): a single `.c` -> `.o` (gcc)
$(DST_DIR)/%.o: %.c
@mkdir -p $(dir $@) && echo + CC $<
@$(CC) -std=gnu11 $(CFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cc` -> `.o` (g++)
$(DST_DIR)/%.o: %.cc
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.cpp` -> `.o` (g++)
$(DST_DIR)/%.o: %.cpp
@mkdir -p $(dir $@) && echo + CXX $<
@$(CXX) -std=c++17 $(CXXFLAGS) -c -o $@ $(realpath $<)
### Rule (compile): a single `.S` -> `.o` (gcc, which preprocesses and calls as)
$(DST_DIR)/%.o: %.S
@mkdir -p $(dir $@) && echo + AS $<
@$(AS) $(ASFLAGS) -c -o $@ $(realpath $<)
### Rule (recursive make): build a dependent library (am, klib, ...)
$(LIBS): %:
@$(MAKE) -s -C $(AM_HOME)/$* archive
### Rule (link): objects (`*.o`) and libraries (`*.a`) -> `IMAGE.elf`, the final ELF binary to be packed into image (ld)
$(IMAGE).elf: $(OBJS) $(LIBS)
@echo + LD "->" $(IMAGE_REL).elf
@$(LD) $(LDFLAGS) -o $(IMAGE).elf --start-group $(LINKAGE) --end-group
### Rule (archive): objects (`*.o`) -> `ARCHIVE.a` (ar)
$(ARCHIVE): $(OBJS)
@echo + AR "->" $(shell realpath $@ --relative-to .)
@$(AR) rcs $(ARCHIVE) $(OBJS)
### Rule (`#include` dependencies): paste in `.d` files generated by gcc on `-MMD`
-include $(addprefix $(DST_DIR)/, $(addsuffix .d, $(basename $(SRCS))))
## 6. Miscellaneous
### Build order control
image: image-dep
archive: $(ARCHIVE)
image-dep: $(OBJS) $(LIBS)
@echo \# Creating image [$(ARCH)]
.PHONY: image image-dep archive run $(LIBS)
### Clean a single project (remove `build/`)
clean:
rm -rf Makefile.html $(WORK_DIR)/build/
.PHONY: clean
### Clean all sub-projects within depth 2 (and ignore errors)
CLEAN_ALL = $(dir $(shell find . -mindepth 2 -name Makefile))
clean-all: $(CLEAN_ALL) clean
$(CLEAN_ALL):
-@$(MAKE) -s -C $@ clean
.PHONY: clean-all $(CLEAN_ALL)