查看: 1695|回复: 0
收起左侧

RISC-V 入门 Part4: 编译、链接、加载

[复制链接]

  离线 

  • TA的每日心情
    奋斗
    2021-3-3 12:32
  • 签到天数: 10 天

    [LV.3]

    发表于 2020-10-20 22:21:32 | 显示全部楼层 |阅读模式

    有人预言,RISC-V或将是继Intel和Arm之后的第三大主流处理器体系。欢迎访问全球首家只专注于RISC-V单片机行业应用的中文网站

    您需要 登录 才可以下载或查看,没有帐号?立即注册

    x
    本帖最后由 皋陶 于 2020-10-20 22:21 编辑

    我们介绍了 RISC-V 的指令,你可以当作介绍了汇编语言。但是,我们现在知道的是:

    • RV32I 的格式都是 32bit 的
    • 以上内容可以以 beq 等格式让读者可读,但是机器执行的还是那6种格式的代码

    我们也了解了 RISC-V 的 calling convention 和 ABI, 这一节介绍程序的编译、链接、加载。基础知识可以阅读 CSAPP 第七章和
    https://zhuanlan.zhihu.com/p/125163040 我之前写的垃圾。不过今天我写的会细一些。

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(1)


    编译



    编译由 .c 转为汇编语言,形式是 .s, 这个我们之前都用过了

    1. ✗ riscv64-unknown-elf-gcc -march=rv32imac -mabi=ilp32 -S
    复制代码

    这样体验一把就行。Compiler 需要走:

    • Lexer: 语法分析,把目标转成 token. 实际上可以借用 Lex 工具,而 Flex 是 Lex 的一个实现。
    • Parser: 将内容变为 AST
    • Semantic Analysis and Optimization: 检查 AST, 然后做一些优化
    • Code generation: 做寄存器 allocation, 代码生成

    实际上,我们生成 -S 也需要指定编译选项,能指定 -O。这里我们可以得到可靠的 RISC-V 的代码。它和我们的程序是逻辑上等价的,当然可能要进行一定的优化。

    注意,在 RISC-V 中,编译是会产生便于理解的伪指令的。

    汇编
    将汇编语言生成 ELF 的 object file, object file 属于 machine language 了。

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(2)


    ELF 文件包括:

    • ELF Header, 以一个 16byte 的序列开始,描述系统 word 大小、字节顺序等
    • .text text segment, 编译程序的机器代码
    • .data 已初始化的 global/static C variable, 即源代码的 static 部分。
      • Local 是在 stack 中的
      • 未初始化的、被初始化为 0 的,在 .bss 中。它不占据实际空间,有点类似 gcc 的 __weak__ .

    • .symtab ,符号表,存放定义、引用的函数、全局变量和不可被 reference 的 static 变量
    • .debug 调试符号表,包含原始文件; .line 同样,包含行号和 .text 的映射。只有 -g 编译才会产生
    • .strtab 字符串表,包含定义的 string 和 section 的名字。

    ELF 具体信息可以看:

    那么我们还要注意,有的 directions, 即汇编指示符,会被丢给链接器,但不产生什么代码

    • .text user text segment 中的片段
    • .data 需要写到 user data segment 中的片段
    • .globl sym 可以从其他文件引用的全局符号
    • .string str 把对应的 C-Style 字符串存在内存中
    • .word w1...wn 把这 n 个连续的符号连续存取

    同时,在链接的时候,会完成伪指令的替换,把它们全部替换成具体的指令(这里可以表示 RV32I 这样的)。

    我们来看看 hello world:

    1. #include <stdio.h>
    2. int main() {
    3.         printf("Hello, %s", "world");
    4.         return 0;
    5. }
    复制代码

    在 RISC-V Book 上,它生成了如下的汇编:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(3)


    其中图 3.6 中用到的指示符有:

    • .text:进入代码段。
    • .align2:后续代码按22字节对齐。
    • .globl main:声明全局符号“main”。
    • .section .rodata:进入只读数据段
    • .balign4:数据段按4字节对齐。
    • .string “Hello, %s!\n”:创建空字符结尾的字符串。
    • .string “world”:创建空字符结尾的字符串。


    tail call optimization

    1. fn:
    2. return foo(x)
    复制代码


    这个时候,正常行为应该是:把 a0 设置 x, 然后返回调用后的 a0, 即:

    • 然后用 j 或者 tail 直接调用 foo(y) ,这玩意会做个保存,把本函数的 ra, sp 保存,这样跳转的话就可以直接跳转到 fn 的调用者。
    在 RISC-V 伪指令中,有一条 tail, 会被解释成

    1. auipc x6, offset[32:12]
    2. jalr x0, x6, offset[11:0]
    复制代码

    用 tail 和 j 来完成上述的 jump,而不再调用前再去设 sp/ra

    这个行为让我有些头晕,我在网上找到了这篇 blog: https://www.sifive.com/blog/all-aboard-part-3-linker-relaxation-in-riscv-toolchain

    上面的链接倒是介绍的比较清楚。

    生成机器代码
    我们描述过 ELF 格式了,我们有下列几个问题

    压缩指令
    RV32C 支持压缩指令:

    • 每条短指令长度为 16bits
    • 必须和 32bits 指令一一对应
    • 只对汇编器和连接器可见,并且是否以短指令取代对应的宽指令由 它们决定。编译器编写者和汇编语言程序员可以幸福地忽略 RV32C 指令及其格式,他们能 感知到的则是最后的程序大小小于大多数其它 ISA 的程序。

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(4)


    RV32C 如上

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(5)


    那么,考虑压缩指令,会有下列问题:

    So the presence of the 16b instructions doesn't need to be known to anybody but the assembler and the RISC-V processor itself!

    Forward Reference
    即假设 L1 --> L2, 但 L1 在 L2 之前,那么这暗示编译器需要一张局部的符号表,并扫描不止一个 pass,来完成这个操作。

    jal/la static 加载
    jal 会跳转一个 imm, 而 static 变量加载中,可能对应的符号来自另一个文件定义的内存中。

    Tables
    为了解决上述问题,有了 symbol table 和 relocation table:

    symbol table 展示可能被其他文件用到的本文件符号,例如 function call 的 label, 和 .data 中可以被外部访问的符号。

    Relocation Table 展示 jal 和 la 中需要重定位的地址。

    链接
    讲 ELF 的 objective code 转化为可执行文件,这一过程被称为 linking, 这一过程有逻辑上如下的流程:

    • 从 .o 文件把 text segment 合在一起
    • 拿到 data segment, 拼接到一起
    • resolve reference, 解决掉跨文件的符号、依赖问题,用绝对的地址填充

    实际上,beq bne jal 这类 PC-relevant 的指令不会被 relocate, 而用 name/label 相关的和 static 的会 relocate.

    加载
    通常 OS 会加载、运行程序:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(6)


    它需要:

    • 读 executable 文件,加载 ELF,来知道 text 和 data 的大小
    • 创建带 stack、text 的地址空间
    • 把 instruction 和 data 拷贝到新的地址空间
    • 拷贝用户的参数,传到栈上,供程序运行
    • 初始化寄存器
    • 跳转到用户程序,并设置 PC


    全流程

    1. #include <stdio.h>
    2. int main(int argc, char* argv[]) {
    3.         int i;
    4.         int sum = 0;
    5.         for (i = 0; i <= 100; i++)
    6.                 sum += i * i;
    7.         printf("The sum of sq from 0 .. 100 is %d\n", sum);
    8. }
    复制代码


    编译后生成:
    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(7)


    assembly 的时候处理伪指令:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(8)


    assembly 的时候生成 symbol table 和 relocation table:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(9)


    以上的信息在链接的时候一起使用。

    动态链接库和静态链接库
    对于静态库而言,它是可执行文件的一部分,库更新了,运行中的程序需要重新编译。这是编译时链接的。

    在 Linux 下,提供了 .a 文件,用于处理,单个文件即使没有用到所有部分,也需要全部加载。

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(10)


    在动态链接库中,允许编译时、运行时链接。

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(11)


    共享库有 .so 文件,引用库的用户可以共享这些数据,而在内存中,共享库的.text 可以共享内存,被多个进程使用:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(12)


    CSAPP 中,指导可以在 dlfcn.h 中使用该功能。

    此外,为了共享,需要处理位置无关代码(Position-Independent Code, PIC)。这需要 -fPIC 选项。

    它在编译时成功设置一个便宜量,并在运行时不改变这个便宜量,让代码能够运行便宜量上的 .text

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(13)


    而 PLT 条目类似懒惰加载,作为链接的表来完成工作:

    国内芯片技术交流-RISC-V 入门 Part4: 编译、链接、加载risc-v单片机中文社区(14)








    上一篇:RISC-V 入门 Part3: 指令格式
    下一篇:RISC-V Part5.1: Datapath 硬件基础
    RISCV作者优文
    全球首家只专注于RISC-V单片机行业应用的中文网站
    回复

    使用道具 举报

    高级模式
    B Color Image Link Quote Code Smilies

    本版积分规则

    关闭

    RISC-V单片机中文网上一条 /2 下一条



    版权及免责声明|RISC-V单片机中文网 |网站地图

    GMT+8, 2024-4-20 05:18 , Processed in 0.863170 second(s), 48 queries .

    快速回复 返回顶部 返回列表