C语言程序的运行

一、程序的运行类型(下面有详细介绍)

在嵌入式系统中,经过编译的C语言程序可以通过操作系统运行,也可以在没有操作系统的情况下运行。程序存放的位置和运行的位置通常是不一样的。一般情况下,经过编译后的程序存储在Flash或者硬盘中,在运行时需要将程序加载到RAM中。嵌入式系统的Nor Flash和硬盘还有一定的差别,在硬盘的程序必须加载到RAM中才可以运行,但是在Nor Flash中的程序可以通过XIP(eXcutive In Place)的方式运行。

在嵌入式系统中,C语言程序的运行包括3种类型:第一种是调试阶段的程序运行,这个阶段程序存放的位置和运行的位置是相同的;第二种是程序直接在Flash中运行(XIP);第三种是将Flash或者硬盘中的程序完全加载到RAM中运行。

二、程序的运行空间及内容

在C语言程序的运行中,存在着两个基本的内存空间,一个是程序的存储空间,另一个是程序的运行空间。程序的存储空间必须包括代码段、只读数据段和读写数据段,程序的加载区域必须包括读写数据段和未初始化数据段。

由于程序放入系统后,必须包括所有需要的信息,代码表示要运行的机器代码,只读数据和读写数据包含程序中预先设置好的数据值,这些都是需要固化存储的,但是未初始化数据没有初值,因此只需要标示它的大小,而不需要存储区域。在程序运行的初始化阶段,将进行加载动作,其中读写数据和未初始化数据都是要在程序中进行"写"操作,因此不可能放在只读的区域内,必须放入RAM中。当然,程序也可以将代码和只读数据放入RAM。在程序运行后,堆和栈将在程序运行过程中动态地分配和释放。

三、C语言可执行代码结构        

一般情况下一个可执行二进制程序(更确切的说在Linux操作系统下为一个进程单元,在UC/OSII中被称为任务)。在存储(没有调入到内存运行)时拥有3个部分,分别是代码段(text)、数据段(data)和BSS段。这3个部分一起组成了该可执行程序的文件。

(1)代码段(text segment) 存放CPU执行的机器指令。通常代码段是可共享的,这使得需要频繁被执行的程序只需要在内存中拥有一份拷贝即可。代码段也通常是只读的,这样可以防止其他程序意外地修改其指令。另外,代码段还规划了局部数据所申请的内存空间信息。         

(2)数据段(data segment) 或称全局初始化数据段/静态数据段(initialized data segment/datasegment)。该段包含了在程序中明确被初始化的全局变量、静态变量(包括全局静态变量和局部静态变量)和常量数据。        

(3)未初始化数据段 亦称BSS(Block Started by Symbol)。该段存入的是全局未初始化变量、静态未初始化变量。

而当程序被加载到内存单元时,则需要另外两个域:堆域和栈域。一个正在运行的C程序占用的内存区域分为代码段、初始化数据段、未初始化数据段(BSS)、堆、栈5个部分。

(4)栈段(stack) 存放函数的参数值、局部变量的值,以及在进行任务切换时存放当前任务的上下文内容。      

(5)堆段(heap) 用于动态内存分配,即使用malloc/free系列函数来管理的内存空间。

四、C语言程序的段

根据C语言的特点,每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。目标代码中各段生成情况如下:

1.代码段(Code

代码段由程序中的各个函数产生,函数的每一个语句将最终经过编译和汇编生成二进制机器代码(具体生成哪种体系结构的机器代码由编译器决定)。

顺序代码

基本数学运算(+,-),逻辑运算(&&,||),位运算(&,|,^)等都属于顺序代码。

选择代码

if,if…else语句等将由编译器生成选择代码。

循环代码

while(),do…while()语句等将由编译器生成循环代码。

对于一些较为复杂的数学运算如除法(\),取余(%)等,虽然它们是C语言的基本运算,但在各种编译系统中的处理方式却不一定相同。根据编译器和体系结构的特点,对它们的处理方式有可能与加减等运算相同,即直接生成处理器的机器代码,也有可能转换成一个库函数的调用。例如,在没有除法指令的体系结构中,编译器在编译a/b这类除法运算的时候,由于处理器没有与其对应的指令,因此会使用调用库函数来模拟除法运算。浮点数的处理与之类似:对于支持浮点运算的体系结构,将直接生成浮点代码;对于不支持浮点数的处理器,编译器将会把每一个浮点运算用库函数调用的方式模拟。

2.只读数据段(ROData

只读数据段由程序中所使用的数据产生,该部分数据的特点是在运行中不需要改变,因此编译器会将该数据放入只读的部分中。C语言的一些语法将生成只读数据段。

只读全局量

例如:定义全局变量const chara[100]={"ABCDEFG"}将生成大小为100个字节的只读数据区,并使用字串"ABCDEFG"初始化。如果定义为const chara[]={"ABCDEFG"},没有指定大小,将根据"ABCDEFG"字串的长度,生成8个字节的只读数据段。

只读局部量

例如:在函数内部定义的变量const charb[100] ={"9876543210"};其初始化的过程和全局量一样。

程序中使用的常量

例如:在程序中使用printf("information\n"),其中包含了字串常量,编译器会自动把常量"information \n"放入只读数据区。

在const chara[100]={"ABCDEFG"}中,定义了100个字节的数据区,但是只初始化了前面的8个字节(7个字符和表示结束的'\0')。在这种用法中,实际后面的字节没有初始化,但是在程序中也不能写,实际上没有任何用处。因此,在只读数据段中,一般都需要做完全的初始化。

3.读写数据段(RWData

读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分数据段和代码段,与只读数据段一样都属于程序中的静态区域,但是具有可写的特点。

已初始化全局变量

例如:在函数外部,定义全局的变量chara[100]={"ABCDEFG"}

已初始化局部静态变量

例如:在函数中定义static charb[100] ={"9876543210"}。函数中由static定义并且已经初始化的数据和数组将被编译为读写数据段。

读写数据区的特点是必须在程序中经过初始化,如果只有定义,没有初始值,则不会生成读写数据区,而会定位为未初始化数据区(BSS)。

4.未初始化数据段(BSS

未初始化数据段常被称之为BSS(英文Block Start by Symbol的缩写)。与读写数据段类似,它也属于静态数据区,但是该段中的数据没有经过初始化。因此它只会在目标文件中被标识,而不会真正称为目标文件中的一个段,该段将会在运行时产生。未初始化数据段只有在运行的初始化阶段才会产生,因此它的大小不会影响目标文件的大小。

const char ro[]={"this is readonly data"}; /* 只读数据段 */
static char rw1[]={"this is global readwrite data"}; 
 /*
已初始化读写数据段 */
char bss_1[100];       /*
未初始化数据段 */
const char* ptrconst = "constant data"; /* "constant data"
放在只读数据段 */
int main()
{
short b;       /* b
放置在栈上,占用2个字节 */
char a[100];    /*
需要在栈上开辟100个字节,a的值是其首地址 */
char s[] = "abcde"; /* s
在栈上,占用4个字节 */
/* "abcde "
本身放置在只读数据存储区,占6字节 */
char *p1;      /* p1
在栈上,占用4个字节  */
char *p2 = "123456";/* "123456"
放置在只读数据存储区,占7字节 */
  /* p2
在栈上,p2指向的内容不能更改。 */
static char rw2[]={"this is local readwrite data"};  
/*
局部已初始化读写数据段 */ 
static char bss_2[100];   /*
局部未初始化数据段 */ 
static int c = 0;     /*
全局(静态)初始化区 */
p1= (char *)malloc(10*sizeof(char));
/*
分配的内存区域在堆区。 */
strcpy(p1, "xxxx");    /* "xxxx"
放置在只读数据存储区,占5字节 */
free(p1);      /*
使用free释放p1所指向的内存 */
return 0;
}

连接器将根据连接顺序将各个文件中的代码段取出,组成可执行程序的代码段,只读数据段和读写数据段。在连接过程中,如果出现符号重名、符号未定义等问题,将会产生连接错误。如果连接成功,将会生成一个统一的文件,这就是可执行程序。

实质上,在目标文件(*.o中未初始化数据段和读写数据段的区别也在于此:读写数据段占用目标文件的容量,而未初始化数据段只是一个标识,不需要占用实际的空间。(但是这个标示存在哪呢,应该占用一些空间)未初始化数据段(BSS)将在程序的初始化阶段中开辟

知识点:在连接过程之前,各个源文件生成目标文件相互没有关系。在连接之后,各目标文件函数和变量可以相互调用和访问,从而被联系在一起。

例如,在某一个C语言的源程序文件中,具有以下的内容:

static char bss_data[2048];
static char rw_data[1024] = {""};

以上定义了两个静态数组,由于bss_data没有初始化,是一个未初始化数据段的数组,编译器只需要标识它的大小即可,而rw_data已经有了一定的初始化数据(即使这个初始化数据没有实际的内容),它建立在已初始化数据段之上,编译器需要在读写数据段内为其开辟空间并赋初值。因此,在生成目标文件的时候,由于rw_data[1024]的存在,目标文件的大小将增加1024字节,而bss_data [2048]虽然定义了2048字节的数组,目标文件的大小并不会因此而增加。

五、C语言程序的运行总结

在具有操作系统的情况下,程序由操作系统加载运行,加载的时候可执行程序可以是一个文件,这个文件将包含程序的主要段以及头信息。对于Linux操作系统,目标程序是可执行的ELF(Executable and linking Format)格式,对于需要在系统直接运行的程序,目标程序应该是纯粹的二进制代码,载入系统后,直接转到代码区地址执行。

事实上,无论运行环境如何,C语言程序在运行时所进行的动作都是类似的。程序在准备开始运行的时候,以下几个条件都是必不可少的:

1.代码段必须位于可运行的存储区。

2.读写数据段必须在可以读写的内存中,而且必须经过初始化。

3.未初始化数据段必须在可以读写的内存中开辟,并被清空。

对于第1点,代码段如果位于可以运行的存储区域中(例如Nor Flash或者RAM),它就不需要加载,可以直接运行;如果代码段位于不能运行的存储区域中(例如:Nand Flash或者硬盘)中,它就必须被加载到RAM运行。

六、RAM调试运行

在嵌入式系统中,这是一种常用的调试方式,而不是通常的运行方式。在通常的运行方式下,程序运行的起始地址一般不可能是RAM。RAM在掉电之后内容会丢失,因此系统上电的时候,RAM中一般不会有有效的程序。但是在程序的调试阶段,可以将程序直接载入RAM,然后在RAM的程序载入地址处运行程序。

这是一种相对简单的形式,因为代码段的存储地址和运行地址是相同的,都是RAM(SDRAM或者SRAM)中的地址。在这种情况下,程序没有运行初始化阶段加载的问题。

从主机向目标机载入程序的时候,程序映像文件中代码段(code或text)、只读数据段、读写数据段依次载入目标系统RAM(SDRAM或者SRAM)的空间中。

程序载入到目标机之后,将从代码区的地址开始运行,在运行的初始化阶段,将开辟未初始化数据区,并将其初始化为0,在运行时将动态开辟堆区和栈区。

在没有操作系统的情况下,开辟内存的工作都是由编译器生成的代码完成的,实现的原理是在映像文件中加入这些代码。主要工作包括:在程序运行时根据实际大小开辟未初始化的数据段;初始化栈区的指针,这个指针和物理内存的实际大小有关;在调用相关函数(malloc、free)时使用堆区,这些函数一般由调用库函数实现。

知识点:程序直接载入RAM运行时,程序的加载位置和运行位置是一致的,因此不存在段复制的问题,需要在初始化阶段开辟未初始化区域,在运行时使用堆栈。

七、固化程序的加载运行

在某些时候,在存放程序的位置是不能运行程序的,例如程序存储在不能以XIP方式运行的Nand-Flash或者硬盘中,在这种情况下,必须将程序完全加载到RAM中才可以运行。

依照这种方式运行程序,需要将Flash中所有的内容全部复制到SDRAM或者SRAM中。在一般情况下,SDRAM或者SRAM的速度要快于Flash。这样做的另外一个好处是可以加快程序的运行速度。也就是说,即使Flash可以运行程序,将程序加载到RAM中运行也还有一定的优势。

这样做也产生了另外一个问题:代码段的载入地址和运行地址是不相同的,载入地址是在ROM(Flash)中,但是运行的地址是在RAM(SDRAM或者SRAM)中。对于这个问题,不同的系统在加载程序的时候有不同的解决方式。

知识点:固化程序在加载运行时,需要复制代码段、只读数据段和读写数据段到RAM中,并另辟未初始化数据段,然后在RAM中运行程序(执行代码段)。

以这种加载方式的运行程序,另外一个重要的问题是:如何把代码移到RAM中。在有操作系统的情况下,代码的复制工作是由操作系统完成的,在没有操作系统的情况下,处理方式相对复杂,程序需要自我复制。显然,这种方式实现的前提是代码最初放置在可以以XIP方式执行的内存中。

程序本身复制的过程也是需要通过程序代码完成的,这时需要程序中的代码根据将包含自己的程序从ROM或者Flash中复制到RAM中。这是一个比较复杂的过程,程序的最前面部分是具有复制功能的代码。系统上电后,从ROM或者Flash起始地址运行,具有复制功能的代码将全部代码段和其他需要复制的部分复制到RAM中,然后跳转到RAM中重新运行程序。

八、固化程序的XIP运行

固化应用是一种嵌入式系统常用的运行方式,其前提是目标代码位于目标系统ROM(Flash)中。ROM中的区域包括映像文件的代码段(code或text)、只读数据段(RO Data)、读写数据段(RW Data)。

代码的运行也是在ROM(Flash)中,因此,在编译过程中代码的存储地址和运行地址是相同的,由于上电时需要启动,因此该代码的位置一般是(0x0)。

在这种应用中,一件重要的事情就是将已初始化读写段的数据从Flash中复制到SDRAM中,由于已初始化读写段既需要固化,也需要在运行时修改,因此这一步是必须有的,在程序的初始化阶段需要完成这一步。

一般来说,在编译过程中需要定义读写段和未初始化段的地址。在程序中可获取这些地址,然后就可以在程序的中加入复制的代码,实现读写段的转移。

知识点:程序在ROM或者Flash中以XIP形式运行的时候,不需要复制代码段和只读数据段,但是需要在RAM中复制读写数据段,并另辟未初始化数据段。

你可能感兴趣的:(C)