找回密码
 立即注册
Qt开源社区 门户 查看内容

RISC-V嵌入式开发准备及入门

2019-7-17 11:21| 发布者: admin| 查看: 4534| 评论: 0

摘要: 本文包括:编译过程简介、嵌入式开发的特点介绍、RISC-V GCC工具链介绍以及RISC-V汇编语言程序设计。注:本文力求通俗易懂,主要面向初学者。一、编译过程简介1 本章概述本章将介绍如何将高层的C/C++语言编写的程序 ...

本文包括:编译过程简介、嵌入式开发的特点介绍、RISC-V GCC工具链介绍以及RISC-V汇编语言程序设计。

注:本文力求通俗易懂,主要面向初学者。

一、编译过程简介

1 本章概述


本章将介绍如何将高层的C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程,该过程即一般编译原理书籍所介绍的过程,包括四个步骤:

  • 预处理(Preprocessing)

  • 编译(Compilation)

  • 汇编(Assembly)

  • 链接(Linking)

本章限于篇幅,将不会对各个步骤的原理进行详解,将仅仅结合Linux自带的GCC工具链对其过程进行简述。感兴趣的读者可以自行查阅其他资料深入学习编译原理的相关知识。

注意:

  • 文中为了简化描述与便于初学者理解,将在Linux操作系统平台上编译一个Hello World程序并在此Linux平台上运行作为示例。而嵌入式开发所使用的交叉编译的使用方法与本文所述的编译过程有所差异,将在第二章《嵌入式开发特点介绍》中对嵌入式系统编译进行更多介绍。

  • 文中使用的是Linux自带的GCC工具链作为演示,而未涉及如何使用RISC-V GCC工具链,将在第三章中对RISC-V GCC工具链进行更多介绍。

2 GCC工具链介绍

2.1 GCC工具链概述


通常所说的GCC是GUN Compiler Collection的简称,是Linux系统上常用的编译工具。GCC实质上不是一个单独的程序,而是多个程序的集合,因此通常称为GCC工具链。工具链软件包括GCC、C运行库、Binutils、GDB等。

  • GCC

    • GCC(GNU C Compiler)是编译工具。本文所要介绍的将C/C++语言编写的程序转换成为处理器能够执行的二进制代码的过程即由编译器完成。有关编译过程的更多介绍请参见后文。

    • GCC既支持本地编译(即在一个平台上编译该平台运行的程序),也支持交叉编译(即在一个平台上编译供另一个平台运行的程序)。

本文为了简化描述与便于初学者理解,将在Linux操作系统平台上编译一个Hello World程序并在此Linux平台上运行作为示例,即为一种本地编译的开发方式。

交叉编译多用于嵌入式系统的开发,有关交叉编译,将在第二章中对嵌入式系统交叉编译进行更多介绍。

  • C运行库

    • 由于C运行库的相关背景知识较多,请参见后文对其单独进行介绍。

  • Binutils

    • 由于Binutils的相关信息较多,请参见后文对其单独进行介绍。

  • GDB

    • GDB(GNU Project Debugger)是调试工具,可以用于对程序进行调试。

2.2 Binutils


一组二进制程序处理工具,包括:addr2line、ar、objcopy、objdump、as、ld、ldd、readelf、size等。这一组工具是开发和调试不可缺少的工具,分别简介如下:

  • addr2line:用来将程序地址转换成其所对应的程序源文件及所对应的代码行,也可以得到所对应的函数。该工具将帮助调试器在调试的过程中定位对应的源代码位置。

  • as:主要用于汇编,有关汇编的详细介绍请参见后文。

  • ld:主要用于链接,有关链接的详细介绍请参见后文。

  • ar:主要用于创建静态库。为了便于初学者理解,在此介绍动态库与静态库的概念:

    • 如果要将多个.o目标文件生成一个库文件,则存在两种类型的库,一种是静态库,另一种是动态库。

    • 在windows中静态库是以 .lib 为后缀的文件,共享库是以 .dll 为后缀的文件。在linux中静态库是以.a为后缀的文件,共享库是以.so为后缀的文件。

    • 静态库和动态库的不同点在于代码被载入的时刻不同。静态库的代码在编译过程中已经被载入可执行程序,因此体积较大。共享库的代码是在可执行程序运行时才载入内存的,在编译过程中仅简单的引用,因此代码体积较小。在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。

    • 如果一个系统中存在多个需要同时运行的程序且这些程序之间存在共享库,那么采用动态库的形式将更节省内存。但是对于嵌入式系统,大多数情况下都是整个软件就是一个可执行程序且不支持动态加载的方式,即以静态库为主。

  • ldd:可以用于查看一个可执行程序依赖的共享库。

  • objcopy:将一种对象文件翻译成另一种格式,譬如将.bin转换成.elf、或者将.elf转换成.bin等。

  • objdump:主要的作用是反汇编。有关反汇编的详细介绍,请参见后文。

  • readelf:显示有关ELF文件的信息,请参见后文了解更多信息。

  • size:列出可执行文件每个部分的尺寸和总尺寸,代码段、数据段、总大小等,请参见后文了解使用size的具体使用实例。

  • Binutils的每个工具的功能均很强大,本节限于篇幅无法详细介绍其功能,读者可以自行查阅资料了解其详情。Binutils还有其他工具,在此不一一赘述,感兴趣的读者可以自行查阅其他资料学习。

2.3 C运行库


为了解释C运行库,需要先回忆一下C语言标准。C语言标准主要由两部分组成:一部分描述C的语法,另一部分描述C标准库。C标准库定义了一组标准头文件,每个头文件中包含一些相关的函数、变量、类型声明和宏定义,譬如常见的printf函数便是一个C标准库函数,其原型定义在stdio头文件中。

C语言标准仅仅定义了C标准库函数原型,并没有提供实现。因此,C语言编译器通常需要一个C运行时库(C Run Time Libray,CRT)的支持。C运行时库又常简称为C运行库。与C语言类似,C++也定义了自己的标准,同时提供相关支持库,称为C++运行时库。

如上所述,要在一个平台上支持C语言,不仅要实现C编译器,还要实现C标准库,这样的实现才能完全支持C标准。glibc(GNU C Library)是Linux下面C标准库的实现,其要点如下:

  • glibc本身是GNU旗下的C标准库,后来逐渐成为了Linux的标准C库。glibc 的主体分布在Linux系统的/lib与/usr/lib目录中,包括 libc 标准 C 函式库、libm数学函式库等等,都以.so做结尾。

    • 注意:Linux系统下面的标准C库不仅有这一个,如uclibc、klibc、以及Linux libc,但是glibc使用最为广泛。而在嵌入式系统中使用较多的C运行库为Newlib,有关Newlib的详细介绍将在第二章《嵌入式开发特点介绍》中进行。

  • Linux系统通常将libc库作为操作系统的一部分,它被视为操作系统与用户程序的接口。譬如:glibc不仅实现标准C语言中的函数,还封装了操作系统提供的系统服务,即系统调用的封装。

    • 通常情况,每个特定的系统调用对应了至少一个glibc 封装的库函数,如系统提供的打开文件系统调用sys_open对应的是glibc中的open函数;其次,glibc 一个单独的API可能调用多个系统调用,如glibc提供的 printf 函数就会调用如 sys_open、sys_mmap、sys_write、sys_close等系统调用;另外,多个 glibc API也可能对应同一个系统调用,如glibc下实现的malloc、free 等函数用来分配和释放内存,都利用了内核的sys_brk的系统调用。

  • 对于C++语言,常用的C++标准库为libstdc++。注意:通常libstdc++与GCC捆绑在一起的,即安装gcc的时候会把libstdc++装上。而glibc并没有和GCC捆绑于一起,这是因为glibc需要与操作系统内核打交道,因此其与具体的操作系统平台紧密耦合。而libstdc++虽然提供了c++程序的标准库,但其并不与内核打交道。对于系统级别的事件,libstdc++会与glibc交互,从而和内核通信。

2.4 GCC命令行选项


GCC有着丰富的命令行选项支持各种不同的功能,本文由于篇幅有限,无法一一赘述,请读者自行查阅相关资料学习。

对于RISC-V的GCC工具链而言,还有其特有的编译选项,将在第三章中介绍RISC-V GCC工具链的更多详情。

3 准备工作

3.1 Linux安装


由于GCC工具链主要是在Linux环境中进行使用,因此本文也将以Linux系统作为工作环境。

对于Linux的安装,准备好自己的电脑环境。如果是个人电脑,推荐如下配置:

  • 使用VMware虚拟机在个人电脑上安装虚拟的Linux操作系统。

  • 由于Linux操作系统的版本众多,推荐使用Ubuntu 16.04版本的Linux操作系统。

有关如何安装VMware以及Ubuntu操作系统本文不做介绍,有关Linux的基本使用本文也不做介绍,请读者自行查阅资料学习。

3.2 准备Hello World程序


为了能够演示编译的整个过程,本节先准备一个C语言编写的简单Hello程序作为示例,其源代码如下所示:
// C语言编写的Hello World程序源代码hello.c

#include<stdio.h> //由于printf函数是一个标准的C语言库函数,其函数原型定义在标准的
                    // C语言stdio头文件中。stdio 是指 “standard input & output”
                 //(标准输入输出)的缩写。所以,源代码中如用到标准输入输出函数时,
                 // 就要包含此头文件

//此程序很简单,仅仅打印一个Hello World的字符串。
intmain(void)
{
 printf("Hello World! \n");
 return0;
}

4 编译过程

4.1 预处理


预处理的过程主要包括以下过程:

  • 将所有的#define删除,并且展开所有的宏定义,并且处理所有的条件预编译指令,比如#if #ifdef #elif #else #endif等。

  • 处理#include预编译指令,将被包含的文件插入到该预编译指令的位置。

  • 删除所有注释“//”和“/* */”。

  • 添加行号和文件标识,以便编译时产生调试用的行号及编译错误警告行号。

  • 保留所有的#pragma编译器指令,后续编译过程需要使用它们。
    使用gcc进行预处理的命令如下:
$ gcc -E hello.c -o hello.i // 将源文件hello.c文件预处理生成hello.i
                       // GCC的选项-E使GCC在进行完预处理后即停止

hello.i文件可以作为普通文本文件打开进行查看,其代码片段如下所示:
// hello.i代码片段

externvoidfunlockfile(FILE *__stream) __attribute__((__nothrow__ , __leaf__));
# 942"/usr/include/stdio.h"34

# 2"hello.c"2

# 3"hello.c"
int
main(void)
{
 printf("Hello World!""\n");
 return0;
}

4.2 编译


编译过程就是对预处理完的文件进行一系列的词法分析,语法分析,语义分析及优化后生成相应的汇编代码。

使用gcc进行编译的命令如下:
$ gcc -S hello.i -o hello.s // 将预处理生成的hello.i文件编译生成汇编程序hello.s
                       // GCC的选项-S使GCC在执行完编译后停止,生成汇编程序

上述命令生成的汇编程序hello.s的代码片段如下所示,其全部为汇编代码。
// hello.s代码片段

main:
.LFB0:
   .cfi_startproc
   pushq   %rbp
   .cfi_def_cfa_offset 16
   .cfi_offset 6, -16
   movq    %rsp, %rbp
   .cfi_def_cfa_register 6
   movl    $.LC0, %edi
   call    puts
   movl    $0, %eax
   popq    %rbp
   .cfi_def_cfa 7, 8
   ret
   .cfi_endproc

4.3 汇编


汇编过程调用对汇编代码进行处理,生成处理器能识别的指令,保存在后缀为.o的目标文件中。由于每一个汇编语句几乎都对应一条处理器指令,因此,汇编相对于编译过程比较简单,通过调用Binutils中的汇编器as根据汇编指令和处理器指令的对照表一一翻译即可。

当程序由多个源代码文件构成时,每个文件都要先完成汇编工作,生成.o目标文件后,才能进入下一步的链接工作。注意:目标文件已经是最终程序的某一部分了,但是在链接之前还不能执行。

使用gcc进行汇编的命令如下:
$ gcc -c hello.s -o hello.o // 将编译生成的hello.s文件汇编生成目标文件hello.o
                       // GCC的选项-c使GCC在执行完汇编后停止,生成目标文件
//或者直接调用as进行汇编
$ as -c hello.s -o hello.o //使用Binutils中的as将hello.s文件汇编生成目标文件

注意:hello.o目标文件为ELF(Executable and Linkable Format)格式的可重定向文件,不能以普通文本形式的查看(vim文本编辑器打开看到的是乱码)。有关ELF文件的更多介绍,请参见后文。

4.4 链接


经过汇编以后的目标文件还不能直接运行,为了变成能够被加载的可执行文件,文件中必须包含固定格式的信息头,还必须与系统提供的启动代码链接起来才能正常运行,这些工作都是由链接器来完成的。

GCC可以通过调用Binutils中的链接器ld来链接程序运行需要的所有目标文件,以及所依赖的其它库文件,最后生成一个ELF格式可执行文件。

如果直接调用Binutils中的ld进行链接,命令如下,则会报出错误:
//直接调用ld试图将hello.o文件链接成为最终的可执行文件hello
$ ld hello.o –o hello
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000b0
hello.o: In function `main':
hello.c:(.text+0xa): undefined reference to `puts'

之所以直接用ld进行链接会报错是因为仅仅依靠一个hello.o目标文件还无法链接成为一个完整的可执行文件,需要明确的指明其需要的各种依赖库和引导程序以及链接脚本,此过程在嵌入式软件开发时是必不可少的。而在Linux系统中,可以直接使用gcc命令执行编译直至链接的过程,gcc会自动将所需的依赖库以及引导程序链接在一起成为Linux系统可以加载的ELF格式可执行文件。使用gcc进行编译直至链接的命令如下:
$ gcc hello.c -o hello  // 将hello.c文件编译汇编链接生成可执行文件hello
                       // GCC没有添加选项,则使GCC一步到位地执行到链接后停
// 止,生成最终的可执行文件

$ ./hello                 //成功执行该文件,在终端上会打印Hello World!字符串 Hello World!

注意:hello可执行文件为ELF(Executable and Linkable Format)格式的可执行文件,不能以普通文本形式的查看(vim文本编辑器打开看到的是乱码)。

在前文介绍了动态库与静态库的差别,与之对应的,链接也分为静态链接和动态链接,其要点如下:

  • 静态链接是指在编译阶段直接把静态库加入到可执行文件中去,这样可执行文件会比较大。链接器将函数的代码从其所在地(不同的目标文件或静态链接库中)拷贝到最终的可执行程序中。为创建可执行文件,链接器必须要完成的主要任务是:符号解析(把目标文件中符号的定义和引用联系起来)和重定位(把符号定义和内存地址对应起来然后修改所有对符号的引用)。



  • 而动态链接则是指链接阶段仅仅只加入一些描述信息,而程序执行时再从系统中把相应动态库加载到内存中去。

    • 在Linux系统中,gcc编译链接时的动态库搜索路径的顺序通常为:首先从gcc命令的参数-L指定的路径寻找;再从环境变量LIBRARY_PATH指定的路径寻址;再从默认路径/lib、/usr/lib、/usr/local/lib寻找。

    • 在Linux系统中,执行二进制文件时的动态库搜索路径的顺序通常为:首先搜索编译目标代码时指定的动态库搜索路径;再从环境变量LD_LIBRARY_PATH指定的路径寻址;再从配置文件/etc/ld.so.conf中指定的动态库搜索路径;再从默认路径/lib、/usr/lib寻找。

    • 在Linux系统中,可以用ldd命令查看一个可执行程序依赖的共享库。



  • 由于链接动态库和静态库的路径可能有重合,所以如果在路径中有同名的静态库文件和动态库文件,比如libtest.a和libtest.so,gcc链接时默认优先选择动态库,会链接libtest.so,如果要让gcc选择链接libtest.a则可以指定gcc选项-static,该选项会强制使用静态库进行链接。以本节的Hello World为例:

    • 如果使用命令“gcc hello.c -o hello”则会使用动态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:
$ gcc hello.c -o hello
$ size hello  //使用size查看大小
  text    data     bss     dec     hex filename
  1183     552       8    1743     6cf     hello
$ ldd hello //可以看出该可执行文件链接了很多其他动态库,主要是Linux的glibc动态库
       linux-vdso.so.1 =>  (0x00007fffefd7c000)
       libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007fadcdd82000)
       /lib64/ld-linux-x86-64.so.2 (0x00007fadce14c000)

  • 如果使用命令“gcc -static hello.c -o hello”则会使用静态库进行链接,生成的ELF可执行文件的大小(使用Binutils的size命令查看)和链接的动态库(使用Binutils的ldd命令查看)如下所示:
$ gcc -static hello.c -o hello
$ size hello //使用size查看大小
    text    data     bss     dec     hex filename
823726    7284    6360  837370   cc6fa     hello //可以看出text的代码尺寸变得极大
$ ldd hello
      not a dynamic executable //说明没有链接动态库

链接器链接后生成的最终文件为ELF格式可执行文件,一个ELF可执行文件通常被链接为不同的段,常见的段譬如.text、.data、.rodata、.bss等段。有关ELF文件和常见段的更多介绍,请参见后文。

4.5 一步到位的编译


从功能上分,预处理、编译、汇编、链接是四个不同的阶段,但GCC的实际操作上,它可以把这四个步骤合并为一个步骤来执行。如下例所示:
$ gcc –o test first.c second.c third.c
     //该命令将同时编译三个源文件,即first.c、second.c和 third.c,然后将它们链接成
//一个可执行文件,名为test。

注意:

  • 一个程序无论有一个源文件还是多个源文件,所有被编译和链接的源文件中必须有且仅有一个main函数。

  • 但如果仅仅是把源文件编译成目标文件,因为不会进行链接,所以main函数不是必需的。

5 分析ELF文件

5.1 ELF文件介绍


在介绍ELF文件之前,首先将其与另一种常见的二进制文件格式bin进行对比:

  • binary文件,其中只有机器码。

  • elf文件除了含有机器码之外还有其它信息,如:段加载地址,运行入口地址,数据段等。

ELF全称Executable and Linkable Format,可执行链接格式。ELF文件格式主要三种:

  • 可重定向(Relocatable)文件:

    • 文件保存着代码和适当的数据,用来和其他的目标文件一起来创建一个可执行文件或者是一个共享目标文件。

  • 可执行(Executable)文件:

    • 文件保存着一个用来执行的程序(例如bash,gcc等)。

  • 共享(Shared)目标文件(Linux下后缀为.so的文件):

    • 即所谓共享库。


5.2 ELF文件的段


ELF文件格式如图1中所示,位于ELF Header和Section Header Table之间的都是段(Section)。一个典型的ELF文件包含下面几个段:

  • .text:已编译程序的指令代码段。

  • .rodata:ro代表read only,即只读数据(譬如常数const)。

  • .data:已初始化的C程序全局变量和静态局部变量。

    • 注意:C程序普通局部变量在运行时被保存在堆栈中,既不出现在.data段中,也不出现在.bss段中。此外,如果变量被初始化值为0,也可能会放到bss段。

  • .bss:未初始化的C程序全局变量和静态局部变量。

    • 注意:目标文件格式区分初始化和未初始化变量是为了空间效率,在ELF文件中.bss段不占据实际的存储器空间,它仅仅是一个占位符。

  • .debug:调试符号表,调试器用此段的信息帮助调试。

  • 上述仅讲解了最常见的节,ELF文件还包含很多其他类型的节,本文在此不做赘述,请感兴趣的读者自行查阅其他资料了解学习。




图1 ELF格式

5.3 查看ELF文件


可以使用Binutils中readelf来查看ELF文件的信息,可以通过readelf --help来查看readelf的选项:
$ readelf --help
Usage: readelf <option(s)> elf-file(s)
Display information about the contents of ELF format files
Options are:
 -a --all               Equivalent to: -h -l -S -s -r -d -V -A -I
 -h --file-header       Display the ELF file header
 -l --program-headers   Display the program headers
    --segments          An alias for --program-headers
 -S --section-headers   Display the sections' header

以本文Hello World示例,使用readelf -S查看其各个section的信息如下:
$ readelf -S hello
There are 31 section headers, starting at offset 0x19d8:

Section Headers:
 [Nr] Name              Type             Address           Offset
      Size              EntSize          Flags  Link  Info  Align
 [ 0]                   NULL             0000000000000000  00000000
      0000000000000000  0000000000000000           0     0     0
……
 [11] .init             PROGBITS         00000000004003c8  000003c8
      000000000000001a  0000000000000000  AX       0     0     4
……
 [14] .text             PROGBITS         0000000000400430  00000430
      0000000000000182  0000000000000000  AX       0     0     16
 [15] .fini             PROGBITS         00000000004005b4  000005b4
……

5.4 反汇编


由于ELF文件无法被当做普通文本文件打开,如果希望直接查看一个ELF文件包含的指令和数据,需要使用反汇编的方法。反汇编是用于调试和定位处理器问题时最常用的手段。
可以使用Binutils中objdump来对ELF文件进行反汇编,可以通过objdump --help来查看其选项:
$ objdump --help
Usage: objdump <option(s)> <file(s)>
Display information from object <file(s)>.
At least one of the following switches must be given:
……
 -D, --disassemble-all    Display assembler contents of all sections
 -S, --source             Intermix source code with disassembly
……

以本文Hello World示例,使用objdump -D对其进行反汇编如下:
$ objdump -D hello
……
0000000000400526 <main>:  // main标签的PC地址
//PC地址:指令编码                  指令的汇编格式
 400526:    55                          push   %rbp
 400527:    48 89 e5                mov    %rsp,%rbp
 40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
 40052f:    e8 cc fe ff ff          callq  400400 <puts@plt>
 400534:    b8 00 00 00 00          mov    $0x0,%eax
 400539:    5d                      pop    %rbp
 40053a:    c3                          retq
 40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
……

使用objdump -S将其反汇编并且将其C语言源代码混合显示出来:
$ gcc -o hello -g hello.c //要加上-g选项
$ objdump -S hello
……
0000000000400526 <main>:
#include <stdio.h>

int
main(void)
{
 400526:    55                          push   %rbp
 400527:    48 89 e5                mov    %rsp,%rbp
 printf("Hello World!" "\n");
 40052a:    bf c4 05 40 00          mov    $0x4005c4,%edi
 40052f:    e8 cc fe ff ff          callq  400400 <puts@plt>
 return 0;
 400534:    b8 00 00 00 00          mov    $0x0,%eax
}
 400539:    5d                          pop    %rbp
 40053a:    c3                          retq
 40053b:    0f 1f 44 00 00          nopl   0x0(%rax,%rax,1)
……

6 嵌入式系统编译的特殊性


为了易于读者理解,本文以一个Hello World程序为例讲解了在Linux环境中的编译过程以帮助初学者入门,但是了解这些基础背景知识对于嵌入式开发还远远不够。
对于嵌入式开发,嵌入式系统的编译有其特殊性,譬如:

  • 嵌入式系统需要使用交叉编译与远程调试的方法进行开发。

  • 需要自己定义引导程序。

  • 需要注意减少代码尺寸。

  • 需要移植printf从而使得嵌入式系统也能够打印输入。

  • 使用Newlib作为C运行库。

  • 每个特定的嵌入式系统都需要配套的板级支持包。

为了易于读者理解,本文使用的是Linux自带的GCC工具链,其并不能反映嵌入式开发的特点。相关内容将在后续章节中介绍。

7 总结


编译原理是一门博大精深的学科,虽然大多数的用户只是将编译器作为一门工具使用而无需关注其内部原理,但是适当的了解编译的过程对于开发大有裨益,尤其是对于嵌入式软件开发而言,更需要了解编译与链接的基本过程。

本文为了简化描述与便于初学者理解,仅仅以在Linux操作系统平台上使用其自带的GCC编译一个Hello World程序作为示例。本文虽面向的是RISC-V嵌入式开发,其使用的RISC-V工具链交叉编译使用方法与本文所述的编译过程有所差异,但是其原理和使用方法大致相同,因此也可以作为初学者的学习参考。

二、嵌入式开发的特点介绍

本文的目的是对嵌入式开发的特点进行简单的科普与回顾,为后续详细介绍“RISC-V GCC工具链”和“RISC-V汇编语言程序设计”打下基础。

在上一章《编译过程简介》中介绍过,嵌入式系统的程序编译过程和开发有其特殊性,下文将分别予以介绍。

1 交叉编译和远程调试


在上一章《编译过程简介》中介绍了如何在Linux系统的PC电脑上开发一个Hello World程序,对其进行编译,然后运行在此电脑上。在这种方式下,我们使用PC电脑上的编译器编译出该PC电脑本身可执行的程序,这种编译方式称之为本地编译。

嵌入式平台上往往资源有限,嵌入式系统(譬如常见ARM MCU或8051单片机)的存储器容量通常只在几KB到几MB之间,且只有闪存而没有硬盘这种大容量存储设备,在这种资源有限的环境中,不可能将编译器等开发工具安装在嵌入式设备中,所以无法直接在嵌入式设备中进行软件开发。因此,嵌入式平台的软件一般在主机PC上进行开发和编译,然后将编译好的二进制代码下载至目标嵌入式系统平台上运行,这种编译方式属于交叉编译。

交叉编译可以简单理解为,在当前编译平台下,编译出来的程序能运行在体系结构不同的另一种目标平台上,但是编译平台本身却不能运行该程序,譬如,在x86平台的PC电脑上编写程序并编译成能运行在ARM平台的程序,编译得到的程序在x86平台上不能运行,必须放到ARM平台上才能运行。

与交叉编译同理,在嵌入式平台上往往也无法运行完整的调试器,因此当运行于嵌入式平台上的程序出现问题时,需要借助主机PC平台上的调试器来对嵌入式平台进行调试。这种调试方式属于远程调试。

常见的交叉编译和远程调试工具是GCC和GDB。在第一章《编译过程简介》中介绍了如何使用Linux自带的GCC本地编译一个Hello World程序并运行。但是,GCC不仅能作为本地编译器,还能作为交叉编译器;同理GDB不仅可以作为本地调试器,还可以作为远程调试器。

当作为交叉编译器之时,GCC通常有不同的命名,譬如:

  • arm-none-eabi-gcc和arm-none-eabi-gdb是面向裸机(Bare-Metal)ARM平台的交叉编译器和远程调试器。

    • 所谓裸机(Bare-Metal)是嵌入式领域的一个常见形态,表示不运行操作系统的系统

  • 而riscv-none-embed-gcc和riscv-none-embed-gdb是面向裸机RISC-V平台的交叉编译器和远程调试器。

    • 在第三章中将介绍RISC-V GCC工具链的更多信息。

2 移植newlib或newlib-nano作为C运行库


newlib是一个面向嵌入式系统的C运行库。在第一章《编译过程简介》中介绍的glibc,newlib实现了大部分的功能函数,但体积却小很多。newlib独特的体系结构将功能实现与具体的操作系统分层,使之能够很好地进行配置以满足嵌入式系统的要求。由于专为嵌入式系统设计,newlib具有可移植性强、轻量级、速度快、功能完备等特点,已广泛应用于各种嵌入式系统中。

由于嵌入式操作系统和底层硬件的多样性,为了能够将C/C++语言所需要的库函数实现与具体的操作系统和底层硬件进行分层,newlib的所有库函数都建立在20个桩函数的基础上,这20个桩函数完成具体操作系统和底层硬件相关的功能:

  • I/O和文件系统访问(open、close、read、write、lseek、stat、fstat、fcntl、link、unlink、rename);

  • 扩大内存堆的需求(sbrk);

  • 获得当前系统的日期和时间(gettimeofday、times);

  • 各种类型的任务管理函数(execve、fork、getpid、kill、wait、_exit);

这20个桩函数在语义、语法上与POSIX(Portable Operating System Interface of UNIX)标准下对应的20个同名系统调用完全兼容。

所以,如果需要移植newlib至某个目标嵌入式平台,成功移植的关键是在目标平台下找到能够与newlib桩函数衔接的功能函数或者实现这些桩函数。在后续文章中将介绍蜂鸟E200的HBird-E-SDK平台如何实现移植实现newlib的桩函数。

注意:newlib的一个特殊版本newlib-nano版本进一步为嵌入式平台减少了代码体积(Code Size),因为newlib-nano提供了更加精简版本的malloc和printf函数的实现,并且对库函数使用GCC的-Os(侧重代码体积的优化)选项进行编译优化。

3 嵌入式引导程序和中断异常处理


在第一章《编译过程简介》中介绍了如何在Linux系统的PC电脑上开发一个Hello World程序,对其进行编译,然后运行在此电脑上。在这种方式下,程序员仅仅只需要关注Hello World程序本身,程序的主体由main函数组织而成,程序员可以无需关注Linux操作系统在运行该程序的main函数之前和之后需要做什么。事实上,在Linux操作系统中运行应用程序(譬如简单的Hello World)时,操作系统需要动态地创建一个进程、为其分配内存空间、创建并运行该进程的引导程序,然后才会开始执行该程序的main函数,待其运行结束之后,操作系统还要清除并释放其内存空间、注销该进程等。

从上述过程中可以看出,程序的引导和清除这些“脏活累活”都是由Linux这样的操作系统来负责进行。但是在嵌入式系统中,程序员除了开发以main函数为主体的功能程序之外,还需要关注如下两个方面:

  • 引导程序:

    • 嵌入式系统上电后需要对系统硬件和软件运行环境进行初始化,这些工作往往由用汇编语言编写的引导程序完成。

    • 引导程序是嵌入式系统上电后运行的第一段软件代码。引导程序对于嵌入式系统非常关键,引导程序所执行的操作依赖于所开发的嵌入式系统的软硬件特性,一般流程包括:初始化硬件、设置异常和中断向量表、把程序拷贝到片上SRAM中、完成代码的重映射等,最后跳转到main函数入口。

    • 后续文章将结合HBird-E-SDK平台的引导程序实例了解引导程序的更多细节。

  • 中断异常处理

    • 中断和异常是嵌入式系统非常重要的一个环节,因此,嵌入式系统软件还必须正确地配置中断和异常处理函数。有关RISC-V架构的中断和异常的详细信息,请参见RISC-V中文书籍《手把手教你设计CPU——RISC-V处理器篇》 中第13章内容《不得不说的故事——中断和异常》。

    • 后续文章将结合HBird-E-SDK程序实例了解如何配置中断和异常处理函数。

4 嵌入式系统链接脚本


在第一章《编译过程简介》中介绍了如何在Linux系统的PC电脑上开发一个Hello World程序,对其进行编译,然后运行在此电脑上。在这种方式下,程序员也无需关心编译过程中的“链接”这一步骤所使用的链接脚本,无需为程序分配具体的内存空间。

但是在嵌入式系统中,程序员除了开发以main函数为主体的功能程序之外,还需要关注“链接脚本”为程序分配合适的存储器空间,譬如程序段放在什么区间、数据段放在什么区间等等。

后续文章将结合HBird-E-SDK的“链接脚本”实例提供更多细节。

5 减少代码体积


嵌入式平台上往往存储器资源有限,嵌入式系统(譬如常见的ARM MCU或8051单片机)的存储器容量通常只在几KB到几MB之间,且只有闪存而没有硬盘这种大容量存储设备,在这种资源有限的环境中,程序的代码体积(Code Size)显得尤其重要,因此,有效地降低降低代码体积(Code Size)是嵌入式软件开发必须要考虑的问题,常见的方法如:

  • 使用newlib-nano作为C运行库以取得较小代码体积(Code Size)的C库函数。

  • 尽量少使用C语言的大型库函数,譬如在正式发行版本的程序中避免使用printf和scanf等函数。

  • 如果在开发的过程中一定需要使用printf函数,可以使用某些自己实现的阉割版printf函数(而不是C运行库中提供的printf函数)以生成较小的代码体积。

  • 除此之外,在C/C++语言的语法和程序开发方面也有众多技巧以取得更小的代码体积(Code Size)。

  • 后续文章将结合HBird-E-SDK平台实例提供更多“减少代码体积”的实现细节。

    减小代码体积(Code Size)的方法很多,本文在此不做一一赘述,请初学的读者自行查阅相关资料进行学习。

6 支持printf函数


在第一章《编译过程简介 》中介绍了如何在Linux系统的PC电脑上开发一个Hello World程序,程序中使用C语言的标准库函数printf打印了一个“Hello World”字符串。该程序在Linux系统里面运行的时候字符串被成功的输出到了Linux的终端界面上。在这个过程中,程序员无需关心Linux系统到底是如何将printf函数的字符串输出到Linux终端上的。事实上,如《编译过程简介》中所述,在Linux本地编译的程序会链接使用Linux系统的C运行库glibc,而glibc充当了应用程序和Linux操作系统之间的接口,glibc提供的 printf 函数就会调用如sys_write等操作系统的底层系统调用函数,从而能够将“字符串”输出到Linux终端上。

从上述过程中可以看出,由于有glibc的支持,所以printf函数能够在Linux系统中正确的进行输出。但是在嵌入式系统中,printf的输出却不那么容易了,基于如下几个原因:

  • 嵌入式系统使用newlib作为C运行库,而newlib的C运行库所提供的printf函数最终依赖于如本文中所介绍的newlib桩函数write,因此必须实现此write函数才能够正确的执行printf函数。

  • 嵌入式系统往往没有“显示终端”存在,譬如常见的单片机其作为一个黑盒子一般的芯片,根本没有显示终端。因此,为了能够支持显示输出,通常需要借助单片机芯片的UART接口将printf函数的输出重新定向到主机PC的COM口上,然后借助主机PC的串口调试助手显示出输出信息。同理,对于scanf输入函数,也需要通过主机PC的串口调试助手获取输入然后通过主机PC的COM口发送给单片机芯片的UART接口。

  • 从以上两点可以看出,嵌入式平台的UART接口非常重要,往往扮演了输出管道的角色,为了能够将printf函数的输出定向到UART接口,需要实现newlib的桩函数write,使其通过编程UART的相关寄存器将字符通过UART接口输出。
    后续文章将结合HBird-E-SDK平台移植printf函数的实例提供更多细节。

7 提供板级支持包


对于特定的嵌入式硬件平台,为了方便用户在硬件平台上开发嵌入式程序,硬件平台一般会提供板级支持包(Board Support Package,BSP)。板级支持包所包含的内容没有绝对的标准,通常说来,其必须包含如下内容:

  • 底层硬件设备的地址分配信息

  • 底层硬件设备的驱动函数

  • 系统的引导程序

  • 中断和异常处理服务程序

  • 系统的链接脚本

  • 如果使用newlib作为C运行库,一般还提供newlib桩函数的实现。

由于板级支持包往往会将很多底层的基础设施和移植工作搭建好,因此应用程序开发人员通常都无需关心本文第1.2节至第1.6节中描述的内容,能够从底层细节中被解放出来避免重复建设而出错。后续文章将结合HBird-E-SDK平台的BSP实例提供更多细节。

三、RISC-V GCC工具链介绍

1 RISC-V GCC工具链种类


在第一章《编译过程简介》中已经介绍了通用的GCC工具链,RISC-V GCC工具链与普通的GCC工具链基本相同,用户可以遵照开源的riscv-gnu-toolchain项目(请在Github中搜索riscv-gnu-toolchain)中的说明自行生成全套的GCC工具链。

由于GCC工具链支持各种不同的处理器架构,因此不同处理器架构的GCC工具链会有不同的命名。遵循GCC工具链的命名规则,当前RISC-V GCC工具链有如下几个版本:

  • 以“riscv64-unknown-linux-gnu-”为前缀的版本,譬如riscv64-unknown-linux-gnu-gcc、riscv64-unknown-linux-gnu-gdb、riscv64-unknown-linux-gnu-ar等。具体的后缀名称与《编译过程简介》中描述的GCC、GDB和Binutils工具相对应。

    • “riscv64-unknown-linux-gnu-”前缀表示该版本的工具链是64位架构的Linux版本工具链。注意:此Linux不是指当前版本工具链一定要运行在Linux操作系统的电脑上,此Linux是指该GCC工具链会使用Linux的Glibc作为C运行库,请参见《编译过程简介》了解Glibc的更多信息。

    • 同理,“riscv32-unknown-linux-gnu-”前缀的版本则是32位架构。

    • 注意:此处的前缀riscv64(还有riscv32的版本)与运行在64位或者32位电脑上毫无关系,此处的64和32是指如果没有通过-march和-mabi选项指定RISC-V架构的位宽,默认将会按照64位还是32位的RISC-V架构来编译程序。有关-march和-mabi选项的含义,请参见第3节。


  • 以“riscv64-unknown-elf-”为前缀的版本,则表示该版本为非Linux(Non-linux)版本的工具链。注意:

    • 此Non-Linux不是指当前版本工具链一定不能运行在Linux操作系统的电脑上,此Non-Linux是指该GCC工具链会使用newlib作为C运行库,请参见第二章《嵌入式开发特点》了解newlib的更多信息。

    • 同上理,此处的前缀riscv64(还有riscv32的版本)与运行在64位或者32位电脑上毫无关系,此处的64和32是指如果没有通过-march和-mabi选项指定RISC-V架构的位宽,默认将会按照64位还是32位的RISC-V架构来编译程序。有关-march和-mabi选项的含义,请参见第3节。


  • 以“riscv-none-embed-”为前缀的版本,则表示是最新为裸机(bare-metal)嵌入式系统而生成的交叉编译工具链,所谓裸机(bare-metal)是嵌入式领域的一个常见形态,表示不运行操作系统的系统。该版本使用新版本的newlib作为C运行库,并且支持newlib-nano,能够为嵌入式系统生成更加优化的代码体积(Code Size)。开源的蜂鸟E203 MCU系统是典型的嵌入式系统,因此将使用“riscv-none-embed-”为前缀的版本作为RISC-V GCC交叉工具链。注意:

    • 此版本编译器由于使用newlib和newlib-nano作为C运行库,所以必须对 newlib底层的桩函数进行移植,否则无法正常使用调用底层桩函数的C函数(譬如printf会调用write桩函数)。

    • 关于Newlib和newlib-nano及其桩函数,请参见第二章《嵌入式开发特点》。

2 riscv-none-embed工具链下载


对于riscv-none-embed版本的工具链而言,为了方便用户直接使用预编译好的工具链,Eclipse开源社区会定期更新发布最新版本的预编译好的RISC-V嵌入式GCC工具链,包括Windows版本和Linux版本。请在谷歌中搜索“releases gnu-mcu-eclipse/riscv-none-gcc”进入网页下载Windows版本或者Linux版本,如图1中所示。对于Linux和Windows版本均只需在相应的操作系统中解压即可使用。



图1 riscv-none-embed工具链下载链接

3 RISC-V GCC工具链的(–march=)和(–mabi=)选项

3.1 (–march=)选项


由于RISC-V的指令集是模块化的指令集,因此在为目标RISC-V平台进行交叉编译之时,需要通过选项指定目标RISC-V平台所支持的模块化指令集组合,该选项为(-march=),有效的选项值如下:

  • rv32i[m][a][f[d]][c]

  • rv32g[c]

  • rv64i[m][a][f[d]][c]

  • rv64g[c]

注意:在上述选项中rv32表示目标平台是32位架构,rv64表示目标平台是64位架构,其他i/m/a/f/d/c/g分别代表了RISC-V模块化指令子集的字母简称。请参见RISC-V中文书籍《手把手教你设计CPU——RISC-V处理器篇》中附录A.1节中关于RISC-V架构指令集的详细中文介绍。

本节后文会介绍(-march=)选项使用的具体实例。

3.2 (–mabi=)选项


由于RISC-V的指令集是模块化的指令集,因此在为目标RISC-V平台进行交叉编译之时,需要通过选项指定嵌入式RISC-V目标平台所支持的ABI函数调用规则(有关ABI函数调用规则的相关信息请参见RISC-V中文书籍《手把手教你设计CPU——RISC-V处理器篇》中附录A的图A-1)。RISC-V定义了两种整数的ABI调用规则和三种浮点ABI调用规则,通过选项(-abi=)指明,有效的选项值如下:

  • ilp32

  • ilp32f

  • ilp32d

  • lp64

  • lp64f

  • lp64d

注意:

  • 在上述选项中两种前缀(ilp32和lp64)表示的含义如下:

  • 前缀ilp32表示目标平台是32位架构,在此架构下,C语言的“int”和“long”变量长度为32比特,“long long”变量为64位;

  • 前缀lp64表示目标平台是64位架构,C语言的“int”变量长度为32比特,而“long”变量长度为64比特。

  • RISC-V的32位和64位架构下更多的数据类型宽度如图2中所示。



图2 RISC-V的32位和64位架构下的数据类型宽度

  • 上述选项中的三种后缀类型(无后缀、后缀f、后缀d)表示的含义如下:

    • 无后缀:在此架构下,如果使用了浮点类型的操作,直接使用RISC-V浮点指令进行支持。但是当浮点数作为函数参数进行传递之时,无论单精度浮点数还是双精度浮点数均需要通过存储器中的堆栈进行传递。

    • f:表示目标平台支持硬件单精度浮点指令。在此架构下,如果使用了浮点类型的操作,直接使用RISC-V浮点指令进行支持。但是当浮点数作为函数参数进行传递之时,单精度浮点数可以直接通过寄存器传递,而双精度浮点数需要通过存储器中的堆栈进行传递。

    • d:表示目标平台支持硬件双精度浮点指令。在此架构下,如果使用了浮点类型的操作,直接使用RISC-V浮点指令进行支持。当浮点数作为函数参数进行传递之时,无论单精度还是双精度浮点数都可以直接通过寄存器传递。

本节后文会介绍(-mabi=)选项使用的具体实例。

3.3 (–march=)和(–mabi=)不同选项编译实例


为了便于读者更加形象地理解(-march=)和(-mabi=)选项的具体含义,下面以一个实例加以介绍。

假设有一段C语言函数代码,如下所示:
//这是一个名为dmul的函数,其有两个参数,为double类型的双精度浮点数
   double dmul(double a, double b) {
     return b * a;
   }

  • 如果使用-march=rv64imafdc -mabi=lp64d的选项组合进行编译,则会生成如下汇编代码:
  $ riscv64-unknown-elf-gcc test.c -march=rv64imafdc -mabi=lp64d -o- -S -O3

//所生成的汇编代码如下,从中可以看出,浮点数乘法操作直接使用了RISC-V的fmul.d指令进行支持,且函数的两个double类型的参数直接使用浮点通用寄存器fa0和fa1进行传递。这是因为:
-    -march选项指明了目标平台支持的模块化指令子集为imafdc,其中包含了F和D指令子集,即支持单精度和双精度浮点指令,因此可以直接使用RISC-V的浮点指令来支持浮点数的操作。
-    -mabi选项指明了后缀“d”,表示当浮点数作为函数参数进行传递之时,无论单精度还是双精度浮点数都可以直接通过寄存器传递。

  dmul:
     fmul.d  fa0,fa0,fa1
     ret

  • 如果使用-march=rv32imac -mabi=ilp32的选项组合进行编译,则会生成如下汇编代码:
 $ riscv64-unknown-elf-gcc test.c -march=rv32imac -mabi=ilp32 -o- -S -O3

//所生成的汇编代码如下,从中可以看出,浮点数乘法操作由C库函数(__muldf3)进行支持,这是因为:
-    -march选项指明了目标平台支持的模块化指令子集为I/M/A/C,其中未包含了F和D指令子集,即不支持单精度和双精度浮点指令,因此无法直接使用RISC-V的浮点指令来支持浮点数的操作。

   dmul:
     mv      a4,a2
     mv      a5,a3
     add     sp,sp,-16
     mv      a2,a0
     mv      a3,a1
     mv      a0,a4
     mv      a1,a5
     sw      ra,12(sp)
     call    __muldf3
     lw      ra,12(sp)
     add     sp,sp,16
     jr      ra

  • 如果使用-march=rv32imafdc -mabi=ilp32的选项组合进行编译,则会生成如下汇编代码:
 $ riscv64-unknown-elf-gcc test.c -march=rv32imafdc -mabi=ilp32 -o- -S -O3

//所生成的汇编代码如下,从中可以看出,浮点数乘法操作直接使用了RISC-V的fmul.d指令进行支持,但是函数的两个浮点类型的参数均通过堆栈进行的传递,这是因为:
-    -march选项指明了目标平台支持的模块化指令子集为I/M/A/F/D/C,其中包含了F和D指令子集,即支持单精度和双精度浮点指令,因此可以直接使用RISC-V的浮点指令来支持浮点数的操作。
-    -mabi选项指明了“无后缀”,表示当浮点数作为函数参数进行传递之时,无论单精度还是双精度浮点数都需要通过堆栈进行传递。

   dmul:
     add     sp,sp,-16    //对堆栈指针寄存器(sp)进行调整,分配堆栈空间
     sw      a0,8(sp)     //将函数参数寄存器a0中的值存入堆栈
     sw      a1,12(sp)    //将函数参数寄存器a1中的值存入堆栈
     fld     fa5,8(sp)    //从堆栈中取回双精度浮点操作数
     sw      a2,8(sp)     //将函数参数寄存器a2中的值存入堆栈
     sw      a3,12(sp)    //将函数参数寄存器a3中的值存入堆栈
     fld     fa4,8(sp)    //从堆栈中取回双精度浮点操作数
     fmul.d  fa5,fa5,fa4  //调用RISC-V的浮点指令进行运算
     fsd     fa5,8(sp)    //将计算结果存回堆栈
     lw      a0,8(sp)      //通过堆栈将结果赋值给函数结果返回寄存器a0
     lw      a1,12(sp)     //通过堆栈将结果赋值给函数结果返回寄存器a1
     add     sp,sp,16      //对堆栈指针寄存器(sp)进行调整,回收堆栈空间
     jr      ra

  • 如果使用-march=rv32imac -mabi=ilp32d的选项组合进行编译,则会报非法错误:
 $ riscv64-unknown-elf-gcc test.c -march=rv32imac -mabi=ilp32d -o- -S -O3

cc1: error: requested ABI requires -march to subsume the 'D' extension

//从中可以看出报非法错误,这是因为:
-    -march选项指明了目标平台支持的模块化指令子集为I/M/A/C,其中未包含了F和D指令子集,即不支持单精度和双精度浮点指令,因此无法直接使用RISC-V的浮点指令来支持浮点数的操作。
-    -mabi选项指明了后缀“d”,表示目标平台支持硬件浮点指令。这一点与-march选项中指明的指令集子集产生了冲突。

3.4 (–march=)和(–mabi=)选项合法组合


虽然(–march=)和(–mabi=)选项理论上可以组成很多种不同的组合,但是目前并不是所有的(–march=)和(–mabi=)选项组合都是合法,目前的riscv-none-embed工具所支持的组合如下:

  • march=rv32i/mabi=ilp32

  • march=rv32ic/mabi=ilp32

  • march=rv32im/mabi=ilp32

  • march=rv32imc/mabi=ilp32

  • march=rv32iac/mabi=ilp32

  • march=rv32imac/mabi=ilp32

  • march=rv32imaf/mabi=ilp32f

  • march=rv32imafc/mabi=ilp32f

  • march=rv32imafdc/mabi=ilp32f

  • march=rv32gc/mabi=ilp32f

  • march=rv64imac/mabi=lp64

  • march=rv64imafdc/mabi=lp64d

  • march=rv64gc/mabi=lp64d

注意:上述有效组合来自于本文撰写时的信息。随着时间推移,新发布的riscv-none-embed工具可能会支持更多的组合,请读者以其最新的发布说明(Release Notes)为准。

4 RISC-V GCC工具链的(–mcmodel=)选项


目前RISC-V GCC工具链认为,在实际的情形中,一个程序的大小一般不会超过4GB的大小,因此在程序内部的寻址空间不能超过4GB的空间。而在64位的架构中,地址空间的大小远远的大于4GB的空间,因此针对RV64架构而言,RISC-V GCC工具链定义了(–mcmodel=)选项用于指定寻址范围的模式,使得编译器在编译阶段能够按照相应的策略编译生成代码。其有效的选项值如下:

  • -mcmodel=medlow

  • -mcmodel=medany

注意:

  • 在RV32架构中,整个地址空间的大小就是4GB,因此(–mcmodel=)选项的任何值对于编译的结果都无影响。

  • RISC-V GCC工具链在未来可能也会支持大于4GB的寻址空间。

medlow和medany两个选项的含义分别解释如下。

  1. (-mcmodel=medlow)选项

(-mcmodel==medlow)选项用于指示该程序的寻址范围固定只能在-2GB至+2GB的空间内。注意:地址区间没有负数可言,-2GB是指整个64位地址空间最高2GB地址区间。
由于此模式的寻址空间是固定的-2GB至+2GB的空间内,因此编译器能够相对生成比较高效的代码,但是由于寻址空间固定,对于整个64位的大多数地址空间都无法访问到,用户需小心使用。

  1. (-mcmodel=medany)选项

(-mcmodel==medlow)选项用于指示该程序的寻址范围可以在任意的一个4G空间内。由于此模式的寻址空间不是固定的,所以相对比较灵活。

5 RISC-V GCC工具链的其他选项


本章仅介绍了RISC-V GCC工具链几个特别的选项,有关RISC-V GCC工具链的完整选项列表和解释,感兴趣的读者可以在谷歌输入“gcc/RISC-V-Options”关键字进行搜索进入相关网页查询,如图3中所示。



图3 RISC-V GCC工具链的完整选项列表和解释

6 RISC-V GCC工具链的预定义宏


RISC-V GCC会根据编译生成若干预定义的宏,在Linux操作环境中可以使用如下方法查看和RISC-V相关的宏:
//首先创建一个空文件
touch empty.h

//使用RISC-V GCC的-E选项对empty.h进行预处理,有关“预处理”的背景知识请参见第一章《编译过程简介》
//通过grep命令对于处理后的文件搜索riscv的关键字

//如果使用-march=rv32imac -mabi=ilp32选项可以看出生成如下预定义宏
riscv-none-embed-gcc -march=rv32imac -mabi=ilp32 -E -dM empty.h | grep riscv

#define __riscv 1
#define __riscv_atomic 1
#define __riscv_cmodel_medlow 1
#define __riscv_float_abi_soft 1
#define __riscv_compressed 1
#define __riscv_mul 1
#define __riscv_muldiv 1
#define __riscv_xlen 32
#define __riscv_div 1

//如果使用-march=rv32imafdc -mabi=ilp32f选项可以看出生成如下预定义宏
riscv-none-embed-gcc -march=rv32imafdc -mabi=ilp32f -E -dM empty.h | grep riscv                                                                                                                  

#define __riscv 1
#define __riscv_atomic 1
#define __riscv_cmodel_medlow 1
#define __riscv_float_abi_single 1
#define __riscv_fdiv 1
#define __riscv_flen 64
#define __riscv_compressed 1
#define __riscv_mul 1
#define __riscv_muldiv 1
#define __riscv_xlen 32
#define __riscv_fsqrt 1
#define __riscv_div 1

7 RISC-V GCC工具链使用实例


后续文章中将结合HBird-E-SDK平台的实例了解如何使用RISC-V GCC工具链进行嵌入式程序的开发与编译。

四、RISC-V汇编语言程序设计

在第一章《编译过程简介》中介绍了C/C++语言如何被编译成为汇编语言,本章将介绍如何直接使用RISC-V架构的汇编语言进行程序设计。

1.1  汇编语言简介


汇编语言(Assembly Language)是一种“低级”语言,汇编语言一听就不高兴了:凭什么说我是低级语言,我哪里低级了。

其实此“低级”非彼“低级”,之所以说汇编语言是一种低级的语言,是因为其面向的是最底层的硬件,直接使用的是处理器的基本指令。因此,相对于抽象层次更高的C/C++语言,汇编语言确实是一门“低级”的语言,此“低级”是指其抽象层次比较低。

由于汇编语言的“低级”属性,它有如下缺点:

  •  由于汇编语言直接接触最底层的硬件,需要对底层硬件非常熟悉才能编写出高效的汇编程序,因此,汇编语言是一门比较难以使用的语言,故而有“汇编语言不会编”的说法。

  • 由于汇编语言的抽象层次很低,因此使用汇编语言

1人点赞鲜花

握手

雷人

路过

鸡蛋

刚表态过的朋友 (1 人)


公告
可以关注我们的微信公众号yafeilinux_friends获取最新动态,或者加入QQ会员群进行交流:190741849、186601429(已满) 我知道了