# 启动文件-进入c语言前需要做什么？【RISC-V】 
## [视频版教程](https://www.bilibili.com/video/BV1Se4y1C7sZ)
## 序言
开发单片机的时候，大家一般默认程序从`void main()`开始执行。  

其实，这是一个表象，单片机在进入`main`函数前需要很多准备工作，只不过芯片厂商已经帮我们做好了，而且一般的开发者不想也没必要关心这些东西，毕竟用不到的东西学得没动力。  

但是，现在我用到了。关注我的朋友们知道，前段时间我开发了一个RISC-V处理器，小麻雀处理器，所有内容都是我一个人做的。其中，为了让它可以运行c语言程序，就必须为它开发板级支持包BSP，让编译器可以编译出可以运行的程序。为了做出一个能用并且好用的BSP，我也不得不学习一下相关内容，做到其然且知其所以然才能更好的前进。  

事实上，通过学习才知道，为了让处理器可以运行c语言程序，处理器、编译器、BSP开发者都为此做了很多的努力。为了理解启动文件的存在意义，下面我将从 编译流程、变量寻址、启动流程 三个层面逐步深入。  

注意，以下内容基于RISC-V架构和GCC编译器，且以小麻雀处理器作为实例，但它的基本原理可以推广到绝大多数的微控制器和编译器。  

## 编译流程
大家在学习c语言的时候，教科书的第一章就是这些，相信大家都有所了解。现在，我带大家从一般性的原理过渡到微控制器的开发流程。  
![编译流程](/doc/图库/小教程/启动文件/编译流程.svg)  
首先，需要明确概念，我们一般说的**编译**，它包含了 **预处理、编译、汇编、链接** 4个步骤。我们一般说的**编译器**，它包含了 **编译、汇编、链接、调试、反汇编** 等功能模块。  
其次，为了编译出单片机可以运行的程序，需要准备好 **C语言代码、汇编代码、链接脚本** 3类文件。  

现在，我们开始。  

**1.C语言代码**  
这是大家最熟悉的东西了。  
C语言通常用于开发应用程序，让单片机可以用在各种各样的地方。  
对于C语言代码，编译器(riscv-none-embed-gcc)可以分析C语言的所描述的行为，并根据处理器的指令集的功能，找到一种功能等价的汇编语言描述，这一步称为**编译**。    

**2.汇编代码**  
这是大家听说过但没有仔细研究的东西。  
汇编代码可以是手写的，也可以是编译生成的。手写汇编一般有两个需求：C语言无法表达的行为，如启动文件；大佬的汇编级优化代码。  
对于汇编代码，汇编器(riscv-none-embed-as)可以分析各种汇编指令和伪代码，并根据处理器的指令定义，转换为处理器可以看懂的二进制文件，这一步称为**汇编**。  
注意，处理器可以看懂这里生成的二进制文件，但不代表它可以正常运行，因为它还是不完整的。  

**3.链接脚本**  
这是大家比较陌生的东西。  
链接脚本搭建了编译器和芯片的桥梁，它告诉链接器(riscv-none-embed-ld)芯片的Flash程序存储器和SRAM数据存储器有多大，地址在哪里，需要分配多大的堆栈空间，各种代码段的地址和长度是多少。  
链接器根据链接脚本提供的内存布局信息，对汇编生成的二进制文件进行组装，把它们分配到合适的区域，生成处理器正常可以运行的程序的程序，这一步称为**链接**。  
链接后便可生成hex、bin、elf等格式的文件，它们可以直接写进单片机运行。    

通过这三类文件，让编译器可以知道，你的芯片是什么样的，你的需求是什么，最后生成单片机可以运行的程序。  

## 变量寻址
看下面这段c语言程序：  
```
#include <stdint.h>
#include <stdio.h>

uint32_t aaa; //全局变量-无初始值-bss段 <-全局指针GP
uint32_t bbb=32; //全局变量-有初始值-data段 <-全局指针GP

void main()
{
    uint32_t ccc; //局部变量-动态分配 <-堆栈指针SP
    ccc=10;
    printf("%c\n", ccc);
}
```
这里面有3种变量：  
- 全局变量，无初始值 **aaa**  
- 全局变量，有初始值 **bbb**  
- 局部变量 **ccc**  

RISC-V架构的寄存器组定义了x1-x31共31个自由使用的可读可写的寄存器，对于手写汇编而言，它们只有地址的区别而没有功能的区别。  

但是，对于汇编/C混合的大型程序而言，为了保证程序可以正确运行，在函数调用/返回过程中可以正确地传递参数，RV定义了一套函数调用规范，声明了寄存器的功能和二进制接口(ABI)名称。
注意，这种定义并非强制性的，而是一种大家约定好并且遵守的方案，目的是让不同的编译器、程序可以在不同的硬件平台上相互兼容。  

![变量寻址](/doc/图库/小教程/启动文件/变量关系.svg)  
如图所示，全局指针与栈指针与C语言变量是直接相关的。    

|寄存器|ABI名称|功能|变量类型|对应变量|
|-    |-      |  - |-|-|
|x2|sp|栈指针 |局部变量|ccc|
|x3|gp|全局指针|全局/静态变量|aaa,bbb|  
  
如图所示，对于RISC-V架构，为了便于寻址，变量在内存的分布定义为：  

|变量类型|初始值|内存分布|对应变量|
|-|-|-|-|
|全局/静态变量|无|bss段|aaa|
|全局/静态变量|有|data段|bbb|
|局部变量|无关|栈区|ccc|

链接器通过链接脚本定义了bss段和data段的大小和位置，程序定义了data段的初始值，执行C语言前需要对bss段和data段做如下处理。  
- **bss段**，默认初始值为0，**数据存储器** 的**对应区域**全部**清0**  
- **data段**，有初始值，将**程序存储器**的**对应数据**搬运至**数据存储器**的**对应区域**  

为了保证有/无初始值的**全局/静态变量**可以正常访问，执行C语言前需要全局指针gp指向链接脚本定义的bss段、data段。  
为了保证**局部变量**可以正常访问，执行C语言前需要栈指针sp指向链接脚本定义的堆栈区域。  

在完成搬运和清空bss段和data段，配置全局指针gp和栈指针sp之前，是不能运行C语言程序的，因此这部分需要用汇编语言实现。  

完成以上任务，为进入`void main()`做准备的汇编程序文件，便称为**启动文件**。  

## 启动文件
下面以小麻雀处理器的启动文件`start.S`作为讲解案例。[点此查看](/doc/小教程/start.S)    
为了强化理解，启动流程参考下图：  
![变量寻址](/doc/图库/小教程/启动文件/启动流程.svg)  
#### 启动文件要放最前  
**第1行**，声明以下内容的段名为`init`。而在链接脚本中，`init`段放在程序存储器的最前面，是处理器上电后第一个执行的程序。  

#### 配置全局指针GP
**第10行**，通过`la`命令，将`__global_pointer$`写入寄存器`gp`。  
其中，`__global_pointer$`是由链接脚本提供的全局指针初始值，编译器会合理分配。  

#### 配置栈指针GP
**第12行**，通过`la`命令，将`_sp`写入寄存器`sp`。  
其中，`_sp`是由链接脚本提供的栈指针位置，开发者应根据数据存储器大小和程序调用层次合理分配。  

#### 加载data段
**第14-25行**，将data段从程序存储器搬运至数据存储器，作为可读可写的变量。  

**第15-17行**，向寄存器a0、a1、a2加载必要数据，地址由链接脚本生成：  

|寄存器|加载值|功能|
|-|-|-|
|a0|程序存储器的data段起始地址|读指针|
|a1|数据存储器的data段起始地址|写指针|
|a2|数据存储器的data段结束地址|结束搬运的地址|

**第18行**，如果a1大于等于a2，表示没有需要搬运的数据，跳过以下循环，执行下面的工作。  
 
**第19-24行**，是一个循环结构。  
1. 读出a0指向的地址，数据写入t0暂存。  
2. t0的数据写入a1指向的地址。  
3. a0+4，指向下一存储单元，因为32bit是4字节。  
4. a1+4，指向下一存储单元，因为32bit是4字节。  
5. 如果a1小于a2，表示未搬运完，跳转至19行，进入下一次循环。 
 
如果a1等于a2，表示已经搬完了最后一个数据，退出循环，执行下面的工作。  

#### 清空bss段
**第28-36行**，工作与data段有点类似，但是只需要清空指定位置的数据。  

**第29-30行**，向寄存器a0、a1加载必要数据，地址由链接脚本生成：   

|寄存器|加载值|功能|
|-|-|-|
|a0|bss段起始地址|指针|
|a1|bss段结束地址|结束清空的地址|

**第31行**，如果a0大于等于a1，表示没有需要清0的空间，跳过以下循环，执行下面的工作。  

**第32-35行**，是一个循环结构。  
1. 清空a0指向的地址  
2. a0+4，指向下一存储单元，因为32bit是4字节。  
3. 如果a0小于a1，表示清0未结束，跳转至32行，进入下一次循环。 
 
如果a0等于a1，表示已经清0了最后一个存储单元，退出循环，执行下面的工作。  

#### 配置中断向量表
这是向量化中断所需的东西。如果没有中断或中断入口固定，可以跳过，非必需。  

#### 系统初始化
**第39行**，这一步已经可以运行C语言程序了，但是在进入`void main()`之前，还需要配置好系统时钟以及各种设计，这里可以自由发挥，非必需。 

#### 进入main函数
**第40行**  
千呼万唤始出来！  
做好了必要的准备工作，进入C语言的世界！  
`void main() {}`

## 总结
启动文件很简单，研究的人很少，背后的意义很深奥。  
这篇文章凝聚了我对嵌入式、C语言、计算机的理解和多年的经验，但即使是我也不能说“我搞懂了”“我是对的”，我只能说出我的理解，如有谬误或疑问，欢迎探讨和交流。  

世界很深奥，越是学习，就能看到越多的未知领域。  
我们不能对未知视而不见，也不能被未知所压垮。  
学习没有起点也没有终点，我们一直在路上。  

   