由于文章比较长,分为两篇来发。这是第一篇(上篇)。
本文将从语言特性、编译器、防御性编程、测试和编程思想这几个方面来讨论如何编写优质嵌入式 C 程序。与很多杂志、书籍不同,本文提供大量真实实例、代码段和参考书目,不仅介绍应该做什么,还重点介绍如何做、以及为什么这样做。编写优质嵌入式 C 程序涉及面十分广,需要程序员长时间的经验积累,本文希望能缩短这一过程。
语言是编程的基石,C 语言诡异且有种种陷阱和缺陷,需要程序员多年历练才能达到较为完善的地步。虽然有众多书籍、杂志、专题讨论过 C 语言的陷阱和缺陷,但这并不影响本节再次讨论它。总是有大批的初学者,前仆后继的倒在这些陷阱和缺陷上,民用设备、工业设备甚至是航天设备都不例外。本节将结合具体例子再次审视它们,希望引起足够重视。深入理解C语言特性,是编写优质嵌入式C程序的基础。
1. if(x=5)
2. {
3. //其它代码
4. }
代码的本意是比较变量 x 是否等于常量 5,但是误将 ”==” 写成了 ”=”,if 语句恒为真。如果在逻辑判断表达式中出现赋值运算符,现在的大多数编译器会给出警告信息。比如 keil MDK 会给出警告提示:“warning: #187-D: use of "=" where"==" may have been intended”,但并非所有程序员都会注意到这类警告,因此有经验的程序员使用下面的代码来避免此类错误:
1. if(5==x)
2. {
3. //其它代码
4. }
2) 复合赋值运算符
复合赋值运算符(+=、*= 等等)虽然可以使表达式更加简洁并有可能产生更高效的机器代码,但某些复合赋值运算符也会给程序带来隐含 Bug,比如”+=”容易误写成 ”=+”,代码如下:
1. tmp=+1;
代码本意是想表达 tmp=tmp+1 ,但是将复合赋值运算符 ”+=” 误写成 ”=+”:将正整数常量 1 赋值给变量 tmp。编译器会欣然接受这类代码,连警告都不会产生。
如果你能在调试阶段就发现这个 Bug,真应该庆祝一下,否则这很可能会成为一个重大隐含 Bug,且不易被察觉。
复合赋值运算符 ”-=” 也有类似问题存在。
3) 其它容易误写
数组常常也是引起程序不稳定的重要因素,C语言数组的迷惑性与数组下标从0开始密不可分,你可以定义int test[30],但是你绝不可以使用数组元素test [30],除非你自己明确知道在做什么。
switch…case 语句可以很方便的实现多分支结构,但要注意在合适的位置添加break 关键字。程序员往往容易漏加 break 从而引起顺序执行多个c ase语句,这也许是 C 的一个缺陷之处。
对于switch…case语句,从概率论上说,绝大多数程序一次只需执行一个匹配的case语句,而每一个这样的case语句后都必须跟一个break。去复杂化大概率事件,这多少有些不合常情。
2) 不能乱加的break
1. network code()
2. {
3. switch(line)
4. {
5. case THING1:
6. {
7. doit1();
8. } break;
9. case THING2:
10. {
11. if(x==STUFF)
12. {
13. do_first_stuff();
14. if(y==OTHER_STUFF)
15. break;
16. do_later_stuff();
17. } /*代码的意图是跳转到这里… …*/
18. initialize_modes_pointer();
19. } break;
20. default :
21. processing();
22. } /*… …但事实上跳到了这里。*/
23. use_modes_pointer(); /*致使modes_pointer未初始化*/
24. }
那个程序员希望从if语句跳出,但他却忘记了break关键字实际上跳出最近的那层循环语句或者switch语句。现在它跳出了switch语句,执行了use_modes_pointer()函数。但必要的初始化工作并未完成,为将来程序的失败埋下了伏笔。
将一个整形常量赋值给变量,代码如下所示:
1. int a=34, b=034;
答案是不相等的。我们知道,16进制常量以’0x’为前缀,10进制常量不需要前缀,那么8进制呢?它与10进制和16进制表示方法都不相同,它以数字’0’为前缀,这多少有点奇葩:三种进制的表示方法完全不相同。如果8进制也像16进制那样以数字和字母表示前缀的话,或许更有利于减少软件Bug,毕竟你使用8进制的次数可能都不会有误使用的次数多!下面展示一个误用8进制的例子,最后一个数组元素赋值错误:
1. a[0]=106; /*十进制数106*/
2. a[1]=112; /*十进制数112*/
3. a[2]=052; /*实际为十进制数42,本意为十进制52*/
指针的加减运算是特殊的。下面的代码运行在32位ARM架构上,执行之后,a和p的值分别是多少?
1. int a=1;
2. int *p=(int *)0x00001000;
3. a=a+1;
4. p=p+1;
对于a的值很容判断出结果为2,但是 p 的结果却是 0x00001004。指针p加1后,p 的值增加了4,这是为什么呢?原因是指针做加减运算时是以指针的数据类型为单位。p+1 实际上是按照公式 p+1*sizeof(int) 来计算的。不理解这一点,在使用指针直接操作数据时极易犯错。
某项目使用下面代码对连续 RAM 初始化零操作,但运行发现有些 RAM 并没有被真正清零。
1. unsigned int *pRAMaddr; //定义地址指针变量
2. for(pRAMaddr=StartAddr;pRAMaddr4)
3. {
4. *pRAMaddr=0x00000000; //指定RAM地址清零
5. }
不知道有多少人最初认为 sizeof 是一个函数。其实它是一个关键字,其作用是返回一个对象或者类型所占的内存字节数,对绝大多数编译器而言,返回值为无符号整形数据。需要注意的是,使用 sizeof 获取数组长度时,不要对指针应用 sizeof 操作符,比如下面的例子:
1. void ClearRAM(char array[])
2. {
3. int i ;
4. for(i=0;i<sizeof(array)/sizeof(array[0]);i++) //这里用法错误,array实际上是指针
5. {
6. array[i]=0x00;
7. }
8. }
9.
10. int main(void)
11. {
12. char Fle[20];
13.
14. ClearRAM(Fle); //只能清除数组Fle中的前四个元素
15. }
我们知道,对于一个数组array[20],我们使用代码sizeof(array)/sizeof(array[0])可以获得数组的元素(这里为20),但数组名和指针往往是容易混淆的,有且只有一种情况下数组名是可以当做指针的,那就是数组名作为函数形参时,数组名被认为是指针,同时,它不能再兼任数组名。注意只有这种情况下,数组名才可以当做指针,但不幸的是这种情况下容易引发风险。在 ClearRAM 函数内,作为形参的 array[] 不再是数组名了,而成了指针。sizeof(array) 相当于求指针变量占用的字节数,在32位系统下,该值为4,sizeof(array)/sizeof(array[0]) 的运算结果也为 4。所以在 main 函数中调用 ClearRAM(Fle),也只能清除数组 Fle 中的前四个元素了。
增量运算符 ”++” 和减量运算符 ”--“ 既可以做前缀也可以做后缀。前缀和后缀的区别在于值的增加或减少这一动作发生的时间是不同的。作为前缀是先自加或自减然后做别的运算,作为后缀时,是先做运算,之后再自加或自减。许多程序员对此认识不够,就容易埋下隐患。下面的例子可以很好的解释前缀和后缀的区别。
1. int a=8,b=2,y;
2. y=a+++--b;
代码执行后,y的值是多少?
1. y=(a++)+(--b);
当赋值给变量y时,a的值为8,b的值为1,所以变量y的值为9;赋值完成后,变量a自加,a的值变为9,千万不要以为y的值为10。这条赋值语句相当于下面的两条语句:
1. y=a+(--b);
2. a=a+1;
为了提高系统效率,逻辑与和逻辑或操作的规定如下:如果对第一个操作数求值后就可以推断出最终结果,第二个操作数就不会进行求值!比如下面代码:
1. if((i>=0)&&(i++ <=max))
2. {
3. //其它代码
4. }
在这个代码中,只有当 i>=0 时,i++才会被执行。这样,i 是否自增是不够明确的,这可能会埋下隐患。逻辑或与之类似。
第一个结构体:
1. struct {
2. char c;
3. short s;
4. int x;
5. }str_test1;
1. struct {
2. char c;
3. int x;
4. short s;
5. }str_test2;
2.2 不可轻视的优先级
比如下面将BCD码转换为十六进制数的代码:
1. result=(uTimeValue>>4)*10+uTimeValue&0x0F;
这里 uTimeValue 存放的BCD码,想要转换成16进制数据,实际运行发现,如果uTimeValue 的值为 0x23,按照我设定的逻辑,result的值应该是 0x17,但运算结果却是0x07。经过种种排查后,才发现’+’的优先级是大于’&’的,相当于(uTimeValue>>4)*10+uTimeValue与0x0F位与,结果自然与逻辑不符。符合逻辑的代码应该是:
1. result=(uTimeValue>>4)*10+(uTimeValue&0x0F);
不合理的 #define 会加重优先级问题,让问题变得更加隐蔽。
1. #define READSDA IO0PIN&(1<<11) //读IO口p0.11的端口状态
2.
3. if(READSDA==(1<<11)) //判断端口p0.11是否为高电平
4. {
5. //其它代码
6. }
编译器在编译后将宏带入,原代码语句变为:
1. if(IO0PIN&(1<<11) ==(1<<11))
2. {
3. //其它代码
4. }
无论如何,在嵌入式编程方面,该掌握的基础知识,偷巧不得。建议花一些时间,将优先级顺序以及容易出错的优先级运算符理清几遍。
1. uint8_t port = 0x5aU;
2. uint8_t result_8;
3. result_8 = (~port) >> 4;
假如我们不了解表达式里的类型提升,认为在运算过程中变量 port 一直是 unigned char 类型的。我们来看一下运算过程:~port 结果为 0xa5,0xa5>>4结果为 0x0a,这是我们期望的值。但实际上,result_8 的结果却是 0xfa!在ARM 结构下,int 类型为 32 位。变量 port 在运算前被提升为 int 类型:~port结果为 0xffffffa5,0xa5>>4 结果为 0x0ffffffa,赋值给变量 result_8,发生类型截断(这也是隐式的!),result_8=0xfa。经过这么诡异的隐式转换,结果跟我们期望的值,已经大相径庭!正确的表达式语句应该为:
1. result_8 = (unsigned char) (~port) >> 4; /*强制转换*/
这种类型提升通常都是件好事,但往往有很多程序员不能真正理解这句话,比如下面的例子(int 类型表示 16 位)。
1. uint16_t u16a = 40000; /* 16位无符号变量*/
2. uint16_t u16b = 30000; /*16位无符号变量*/
3. uint32_t u32x; /*32位无符号变量 */
4. uint32_t u32y;
5. u32x = u16a + u16b; /* u32x = 70000还是4464 ? */
6. u32y =(uint32_t)(u16a + u16b); /* u32y = 70000 还是4464 ? */
u32x 和 u32y 的结果都是 4464(70000%65536)!不要认为表达式中有一个高类别 uint32_t 类型变量,编译器都会帮你把所有其他低类别都提升到 uint32_t类型。正确的书写方式:
1. u32x = (uint32_t)u16a +(uint32_t)u16b; 或者:
2. u32x = (uint32_t)u16a + u16b;
1. uint16_t u16a,u16b,u16c;
2. uint32_t u32x;
3. u32x = u16a + u16b + (uint32_t)u16c;/*错误写法,u16a+ u16b仍可能溢出*/
当作为函数的参数被传递时,char 和 short 会被转换为 int,float 会被转换为 double。
精度低的类型强制转换为精度高的类型时,如果两种类型具有相同的符号,那么没什么问题;需要注意的是负的有符号精度低类型强制转换为无符号精度高类型时,会不直观的执行符号扩展,例如:
1. unsigned int bob;
2. signed char fred = -1;
3.
4. bob=(unsigned int)fred; /*发生符号扩展,此时bob为0xFFFFFFFF*/
编译器的语义检查很弱小,甚至还会“掩盖”错误。现代的编译器设计是件浩瀚的工程,为了让编译器设计简单一些,目前几乎所有编译器的语义检查都比较弱小。为了获得更快的执行效率,C 语言被设计的足够灵活且几乎不进行任何运行时检查,比如数组越界、指针是否合法、运算结果是否溢出等等。这就造成了很多编译正确但执行奇怪的程序。
C语言足够灵活,对于一个数组 test[30],它允许使用像 test[-1] 这样的形式来快速获取数组首元素所在地址前面的数据;允许将一个常数强制转换为函数指针,使用代码 (((void()())0))() 来调用位于 0 地址的函数。C 语言给了程序员足够的自由,但也由程序员承担滥用自由带来的责任。
1. unsigned char i; //例程1
2. for(i=0;i<256;i++)
3. {
4. //其它代码
5. }
1. unsigned char i; //例程2
2. for(i=10;i>=0;i--)
3. {
4. //其它代码
5. }
1. if(a>b); //这里误加了一个分号
2. a=b; //这句代码一直被执行
1. if(n<3)
2. return //这里少加了一个分号
3. logrec.data=x[0];
4. logrec.time=x[1];
5. logrec.code=x[2];
1. int SensorData[30];
2. //其他代码
3. for(i=30;i>0;i--)
4. {
5. SensorData[i]=…;
6. //其他代码
7. }
这里声明了拥有 30 个元素的数组,不幸的是 for 循环代码中误用了本不存在的数组元素 SensorData[30],但 C 语言却默许这么使用,并欣然的按照代码改变了数组元素 SensorData[30] 所在位置的值, SensorData[30] 所在的位置原本是一个 LCD 显示变量,这正是显示屏上的那个值不正常被改变的原因。真庆幸这么轻而易举的发现了这个 Bug。
1. int SensorData[30];
1. extern int SensorData[];
1. char * func(char SensorData[30])
2. {
3. unsignedint i;
4. for(i=30;i>0;i--)
5. {
6. SensorData[i]=…;
7. //其他代码
8. }
9. }
我们常常用数组来缓存通讯中的一帧数据。在通讯中断中将接收的数据保存到数组中,直到一帧数据完全接收后再进行处理。即使定义的数组长度足够长,接收数据的过程中也可能发生数组越界,特别是干扰严重时。这是由于外界的干扰破坏了数据帧的某些位,对一帧的数据长度判断错误,接收的数据超出数组范围,多余的数据改写与数组相邻的变量,造成系统崩溃。由于中断事件的异步性,这类数组越界编译器无法检查到。
1. __irq ExintHandler(void)
2. {
3. unsignedchar DataBuf[50];
4. GetData(DataBug); //从硬件缓冲区取一帧数据
5. //其他代码
6. }
由于存在多个无线传感器近乎同时发送数据的可能加之 GetData() 函数保护力度不够,数组 DataBuf 在取数据过程中发生越界。由于数组 DataBuf 为局部变量,被分配在堆栈中,同在此堆栈中的还有中断发生时的运行环境以及中断返回地址。溢出的数据将这些数据破坏掉,中断返回时 PC 指针可能变成一个不合法值,硬件异常由此产生。
做嵌入式设备开发,如果不对 volatile 修饰符具有足够了解,实在是说不过去。volatile 是 C 语言 32 个关键字中的一个,属于类型限定符,常用的 const 关键字也属于类型限定符。
这个特性在嵌入式应用中很有用,比如你的 IO 口的数据不知道什么时候就会改变,这就要求编译器每次都必须真正的读取该 IO 端口。这里使用了词语“真正的读”,是因为由于编译器的优化,你的逻辑反应到代码上是对的,但是代码经过编译器翻译后,有可能与你的逻辑不符。你的代码逻辑可能是每次都会读取 IO 端口数据,但实际上编译器将代码翻译成汇编时,可能只是读一次 IO 端口数据并保存到寄存器中,接下来的多次读 IO 口都是使用寄存器中的值来进行处理。因为读写寄存器是最快的,这样可以优化程序效率。与之类似的,中断里的变量、多线程中的共享变量等都存在这样的问题。
一个程序模块通常由两个文件组成,源文件和头文件。如果你在源文件定义变量:
1. unsigned int test;
1. extern unsigned long test;
编译器会提示一个语法错误:变量 ’ test’ 声明类型不一致。但如果你在源文件定义变量:
1. volatile unsigned int test;
在头文件中这样声明变量:
1. extern unsigned int test; /*缺少volatile限定符*/
1. volatile unsigned int TimerCount=0;
该变量用来在一个定时器中断服务程序中进行软件计时:
1. TimerCount++;
1. extern unsigned int TimerCount; //这里漏掉了类型限定符volatile
在模块B中,要使用TimerCount变量进行精确的软件延时:
1. #include “…A.h” //首先包含模块A的头文件
2. //其他代码
3. TimerCount=0;
4. while(TimerCount<=TIMER_VALUE); //延时一段时间(感谢网友chhfish指出这里的逻辑错误)
5. //其他代码
为了更容易的理解编译器如何处理 volatile 限定符,这里给出未使用 volatile 限定符和使用 volatile 限定符程序的反汇编代码:
没有使用关键字 volatile,在 keil MDK V4.54 下编译,默认优化级别,如下所示(注意最后两行):
122: unIdleCount=0;
2. 123:
3. 0x00002E10 E59F11D4 LDR R1,[PC,#0x01D4]
4. 0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
5. 0x00002E18 E1A00005 MOV R0,R5
6. 0x00002E1C E5815000 STR R5,[R1]
7. 124: while(unIdleCount!=200); //延时2S钟
8. 125:
9. 0x00002E20 E35000C8 CMP R0,#0x000000C8
10. 0x00002E24 1AFFFFFD BNE 0x00002E20
122: unIdleCount=0;
2. 123:
3. 0x00002E10 E59F01D4 LDR R0,[PC,#0x01D4]
4. 0x00002E14 E3A05000 MOV R5,#key1(0x00000000)
5. 0x00002E18 E5805000 STR R5,[R0]
6. 124: while(unIdleCount!=200); //延时2S钟
7. 125:
8. 0x00002E1C E5901000 LDR R1,[R0]
9. 0x00002E20 E35100C8 CMP R1,#0x000000C8
10. 0x00002E24 1AFFFFFC BNE 0x00002E1C
可以看到,如果没有使用 volatile 关键字,程序一直比较 R0 内数据与 0xC8 是否相等,但 R0 中的数据是 0,所以程序会一直在这里循环比较(死循环);再看使用了 volatile 关键字的反汇编代码,程序会先从变量中读出数据放到 R1 寄存器中,然后再让 R1 内数据与 0xC8 相比较,这才是我们 C 代码的正确逻辑!
ARM 架构下的编译器会频繁的使用堆栈,堆栈用于存储函数的返回值、AAPCS规定的必须保护的寄存器以及局部变量,包括局部数组、结构体、联合体和C++的类。默认情况下,堆栈的位置、初始值都是由编译器设置,因此需要对编译器的堆栈有一定了解。从堆栈中分配的局部变量的初值是不确定的,因此需要运行时显式初始化该变量。一旦离开局部变量的作用域,这个变量立即被释放,其它代码也就可以使用它,因此堆栈中的一个内存位置可能对应整个程序的多个变量。
1. unsigned intGetTempValue(void)
2. {
3. unsigned int sum; //定义局部变量,保存总值
4. for(i=0;i<10;i++)
5. {
6. sum+=CollectTemp(); //函数CollectTemp可以得到当前的温度值
7. }
8. return (sum/10);
9. }
由于一旦程序离开局部变量的作用域即被释放,所以下面代码返回指向局部变量的指针是没有实际意义的,该指针指向的区域可能会被其它程序使用,其值会被改变。
1. char * GetData(void)
2. {
3. char buffer[100]; //局部数组
4. …
5. return buffer;
6. }
由于编译器的语义检查比较弱,我们可以使用第三方代码分析工具,使用这些工具来发现潜在的问题,这里介绍其中比较著名的是 PC-Lint。
Configuration File:指定配置文件的路径,该配置文件由MDK编译器提供。
检查当前文件。
菜单Tools---Lint All C-Source Files
检查所有C源文件。
推荐阅读:Keil MDK免费社区版(附赠安装包)
C 语言标准特别的规定某些行为是未定义的,编写未定义行为的代码,其输出结果由编译器决定!C 标准委员会定义未定义行为的原因如下:
编译器开发商可以通过未定义行为对语言进行扩展
1. r = 1 * a[i++] + 2 * a[i++] + 3 * a[i++];
不同的编译器可能有着不同的汇编代码,可能是先执行 i++ 再进行乘法和加法运行,也可能是先进行加法和乘法运算,再执行 i++,因为这句代码在一个表达式中出现了连续的自增并作用于同一变量。更加隐蔽的是自增自减在表达式中出现一次,但作用的变量多次出现,比如:
1. a[i] = i++; /* 未定义行为 */
先执行 i++ 再赋值,还是先赋值再执行 i++ 是由编译器决定的,而两种不同的执行顺序的结果差别是巨大的。
函数如果有多个实参,这些实参的求值顺序是由编译器决定的,比如:
1. printf("%d %d\n", ++n, power(2, n)); /* 未定义行为 */
1. int value1,value2,sum
2.
3. //其它操作
4. sum=value1+value; /*sum可能发生溢出*/
有符号数右移、移位的数量是负值或者大于操作数的位数
除数为零
malloc()、calloc() 或 realloc() 分配零字节内存
了解 C 语言未定义行为
寻求工具帮助
编译器警告信息以及 PC-Lint 等静态检查工具能够发现很多未定义行为并警告,要时刻关注这些工具反馈的信息;
总结并使用一些编码标准
1)避免构造复杂的自增或者自减表达式,实际上,应该避免构造所有复杂表达式;
比如 a[i] = i++; 语句可以改为 a[i] = i; i++; 这两句代码。
2)只对无符号操作数使用位操作;
1. int value1,value2,sum;
2.
3. //其它代码
4. if((value1>0 && value2>0 && value1>(INT_MAX-value2))||
5. (value1<0 && value2<0 && value1<(INT_MIN-value2)))
6. {
7. //处理错误
8. }
9. else
10. {
11. sum=value1+value2;
12. }
int value1, value2, sum;
unsigned int usum = (unsigned int)value1 + value2;
if((usum ^ value1) & (usum ^ value2) & INT_MIN)
{
/*处理溢出情况*/
}
else
{
sum = value1 + value2;
}
使用的原理解释一下,因为在加法运算中,操作数 value1 和 value2 只有符号相同时,才可能发生溢出,所以我们先将这两个数转换为无符号类型,两个数的和保存在变量 usum 中。如果发生溢出,则 value1、value2 和 usum 的最高位(符号位)一定不同,表达式 (usum ^ value1) & (usum ^ value2) 的最高位一定为1,这个表达式位与(&)上 INT_MIN 是为了将最高位之外的其它位设置为0。
很多引入了未定义行为的程序也能运行良好,这要归功于编译器处理未定义行为的策略。不是你的代码写的正确,而是恰好编译器处理策略跟你需要的逻辑相同。了解编译器的未定义行为处理策略,可以让你更清楚的认识到那些引入了未定义行为程序能够运行良好是多么幸运的事,不然多换几个编译器试试!
以 Keil MDK 为例,列举常用的处理策略如下:
2)对于 int 类的值:超过 31 位的左移结果为零;无符号值或正的有符号值超过31 位的右移结果为零。负的有符号值移位结果为 -1。
对于结构体填充,根据定义结构的方式,keil MDK 编译器用以下方式的一种来填充结构:
编译器不对声明为 volatile 类型的数据进行优化;
__nop():延时一个指令周期,编译器绝不会优化它。如果硬件支持NOP指令,则该句被替换为NOP指令,如果硬件不支持NOP指令,编译器将它替换为一个等效于NOP的指令,具体指令由编译器自己决定;
__align(n):指示编译器在 n 字节边界上对齐变量。对于局部变量,n 的值为1、2、4、8;
attribute((at(address))):可以使用此变量属性指定变量的绝对地址;
__inline:提示编译器在合理的情况下内联编译C或C++ 函数;
1. unsigned int g_unRunFlag=0xA5;
2. static unsigned int s_unCountFlag=0x5A;
我曾做过一个项目,项目中的一个设备需要在线编程,也就是通过协议,将上位机发给设备的数据通过在应用编程(IAP)技术写入到设备的内部 Flash 中。我将内部 Flash 做了划分,一小部分运行程序,大部分用来存储上位机发来的数据。随着程序量的增加,在一次更新程序后发现,在线编程之后,设备运行正常,但是重启设备后,运行出现了故障!经过一系列排查,发现故障的原因是一个全局变量的初值被改变了。这是件很不可思议的事情,你在定义这个变量的时候指定了初始值,当你在第一次使用这个变量时却发现这个初值已经被改掉了!这中间没有对这个变量做任何赋值操作,其它变量也没有任何溢出,并且多次在线调试表明,进入 main 函数的时候,该变量的初值已经被改为一个恒定值。
要想知道为什么全局变量的初值被改变,就要了解这些初值编译后被放到了二进制文件的哪里。在此之前,需要先了解一点链接原理。
其实MDK编译器的输出文件中有一个“工程名.map”文件,里面记录了代码、变量、堆栈的存储位置,通过这个文件,可以查看使用的变量被分配到 RAM 的哪个位置。要生成这个文件,需要在 Options for Targer 窗口,Listing 标签栏下,勾选 Linker Listing 前的复选框,如图3-1所示。
3.4.4 默认情况下,栈被分配到 RAM 的哪个地方?
MDK 中,我们只需要在配置文件中定义堆栈大小,编译器会自动在 RAM 的空闲区域选择一块合适的地方来分配给我们定义的堆栈,这个地方位于 RAM 的哪个地方呢?
通过查看 MAP 文件,原来 MDK 将堆栈放到程序使用到的 RAM 空间的后面,比如你的 RAM 空间从 0x4000 0000 开始,你的程序用掉了 0x200 字节 RAM,那么堆栈空间就从 0x4000 0200 处开始。
使用了多少堆栈,是否溢出?
在进入 main() 函数之前,MDK 会把未初始化的 RAM 给清零的,我们的 RAM 可能很大,只使用了其中一小部分,MDK 会不会把所有 RAM 都初始化呢?
答案是否定的,MDK 只是把你的程序用到的 RAM 以及堆栈 RAM 给初始化,其它 RAM 的内容是不管的。如果你要使用绝对地址访问 MDK 未初始化的 RAM,那就要小心翼翼的了,因为这些 RAM 上电时的内容很可能是随机的,每次上电都不同。
MDK 编译程序生成的可执行文件中,每个输出段都最多有三个属性:RO 属性、RW 属性和 ZI 属性。对于一个全局变量或静态变量,用 const 修饰符修饰的变量最可能放在 RO 属性区,初始化的变量会放在 RW 属性区,那么剩下的变量就要放到 ZI 属性区了。默认情况下,ZI 属性区的数据在每次复位后,程序执行main 函数内的代码之前,由编译器“自作主张”的初始化为零。所以我们要在 C 代码中设置一些变量在复位后不被零初始化,那一定不能任由编译器“胡作非为”,我们要用一些规则,约束一下编译器。
1: LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2: ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x10000000 0x0000A000 { ; RW data
8: .ANY (+RW +ZI)
9: }
10: MYRAM 0x1000A000 UNINIT 0x00002000 {
11: .ANY (NO_INIT)
12: }
13: }
那么,如果在程序中有一个数组,你不想让它复位后零初始化,就可以这样来定义变量:
1. unsigned char plc_eu_backup[32] __attribute__((at(0x1000A000)));
unsigned char plc_eu_backup[32] __attribute__((section("NO_INIT"),zero_init));
假如该模块名字为 test.c,修改分散加载文件如下所示:
1: LR_IROM1 0x00000000 0x00080000 { ; load region size_region
2: ER_IROM1 0x00000000 0x00080000 { ; load address = execution address
3: *.o (RESET, +First)
4: *(InRoot$$Sections)
5: .ANY (+RO)
6: }
7: RW_IRAM1 0x10000000 0x0000A000 { ; RW data
8: .ANY (+RW +ZI)
9: }
10: RW_IRAM2 0x1000A000 UNINIT 0x00002000 {
11: test.o (+ZI)
12: }
13: }
在该模块定义时变量时使用如下方法:
这里,变量属性修饰符 __attribute__((zero_init)) 用于将未初始化的变量放到 ZI 数据节中变量,其实 MDK 默认情况下,未初始化的变量就是放在 ZI 数据区的。
原文:http://blog.csdn.net/zhzht19861011/article/details/45508029
文章来源于网络,版权归原作者所有,如有侵权,请联系删除。
关注我【一起学嵌入式】,一起学习,一起成长。
觉得文章不错,点击“分享”、“赞”、“在看” 呗!