发帖数

53

原创数

53

关注者

11

阅读数

9261

点赞数

1

黄忠

  • ​单片机C语言程序的存储区域解读


    C语言代码(文本文件)形成可执行程序(二进制文件),需要经过编译-汇编-链接三个阶段。编译过程把C语言文本文件生成汇编程序,汇编过程把汇编程序形成二进制机器代码,链接过程则将各个源文件生成的二进制机器代码文件组合成一个文件。

    C语言编写的程序经过编译-链接后,将形成一个统一文件,它由几个部分组成。在程序运行时又会产生其他几个部分,各个部分代表了不同的存储区域:

    1、代码段(CodeText

    代码段由程序中执行的机器代码组成。在C语言中,程序语句进行编译后,形成机器代码。在执行程序的过程中,CPU的程序计数器指向代码段的每一条机器代码,并由处理器依次运行。

    2、只读数据段(RO data

    只读数据段是程序使用的一些不会被更改的数据,使用这些数据的方式类似查表式的操作,由于这些变量不需要更改,因此只需要放置在只读存储器中即可。

    3、已初始化读写数据段(RW data

    已初始化数据是在程序中声明,并且具有初值的变量,这些变量需要占用存储器的空间,在程序执行时它们需要位于可读写的内存区域内,并具有初值,以供程序运行时读写。

    4、未初始化数据段(BSS

    未初始化数据是在程序中声明,但是没有初始化的变量,这些变量在程序运行之前不需要占用存储器的空间。

    5、堆(heap

    堆内存只在程序运行时出现,一般由程序员分配和释放。在具有操作系统的情况下,如果程序没有释放,操作系统可能在程序(例如一个进程)结束后回收内存。

    6、栈(stack

    栈内存只在程序运行时出现,在函数内部使用的变量、函数的参数以及返回值将使用栈空间,栈空间由编译器自动分配和释放。

    C语言目标文件的内存布局如下图:

    图片32.png

    代码段、只读数据段、读写数据段、未初始化数据段属于静态区域,而堆和栈属于动态区域。代码段、只读数据段和读写数据段将在连接之后产生,未初始化数据段将在程序初始化的时候开辟,而堆和栈将在程序的运行中分配和释放。

    C语言程序分为映像和运行时两种状态。在编译-链接后形成的映像中,将只包含代码段(Text)、只读数据段(RO Data)和读写数据段(RW Data)。在程序运行之前,将动态生成未初始化数据段(BSS),在程序的运行时还将动态形成堆(Heap)区域和栈(Stack)区域。

    一般来说,在静态的映像文件中,各个部分称之为节(Section),而在运行时的各个部分称之为段(Segment)。如果不详细区分,可以统称为段。

    每一个源文件生成的目标代码有了这些段的区分,那么每个段就能表示出源文件想要表达的信息和功能。下一篇我们一起深入分析下在这些段中是怎么体现的源码的信息的。


    收藏 1 回复 0 浏览 33
  • 程序的链接过程和存储区解读

    大家好,我们张飞实战电子黄忠老师,今天我们讲解程序的链接和存储区的解读。

    根据C语言的特点,每一个源程序生成的目标代码将包含源程序所需要表达的所有信息和功能。有些时候很有必要从这些段中来分析实际使用情况和改进空间。目标代码中各段生成情况如下:

    1、代码段(Code

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

    2、只读数据段(RO Data

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

    ① 只读全局变量

    例如:定义全局变量 const char a[100]={ABCDEFG};

    这个是生成大小为100个字节的只读数据区,并使用字符串“ABCDEFG”初始化。如果定义的时候没有指定大小,那么根据初始化的字符串长度生成相应大小的只读数据段。

    ② 只读局部变量

    例如:在函数内部定义的只读变量 const char b[100]={9876543210};

    ③ 程序中使用的常量

    例如:在程序中使用printfinformation n,其中“information n”是字符串常量, 编译器会自动把常量放入只读数据区。

    注意:上面两个变量定义,定义100个大小的区域,但是只初始化前面几个字节,实际后面的字节没有初始化,但是在程序中也不能写,实际是没有任何用处的。所以定义只读的时候需要做完全的初始化。

    3、读写数据段(RW Data

    读写数据段表示了在目标文件中一部分可以读也可以写的数据区,在某些场合它们又被称为已初始化数据段。这部分是属于程序中的静态区域。

    ①已初始化全局静态变量

    在函数外部定义的全局的变量,并且初始化。(static是限制作用域的)

    ②已初始化局部静态变量

    在函数中定义的由static定义并且已经初始化的数据或者数组。

    注意:定义的变量要有初始化才会在读写数据区。

    4、未初始化数据段(BSS

    这个段也属于静态数据区。但是没有初始化,所以在目标文件中会有标识,而不会真正称为目标文件中的一个段,这个段会在运行时产生,所以它的大小不会影响目标文件的大小。

    图片33.png 

    比如上面这个图就是通常我们编译后获得的。当你的方案选型是一个空间很小的处理器的时候很有必要了解这些存储的区域都存的是什么,方便处理冗余或者修改方案。

    上面我们了解了程序对应的存储空间,程序是怎么对号入座到这些存储区的呢?一起来看下吧,也没想象的那么神秘。

    我们每一个C语言源程序(*.c)经过编译生成目标文件(.o),目标文件就包含前面我们说的代码段、只读数据段、读写数据段。未初始化数据段、堆和栈不会占用目标文件的空间。

    那么可执行程序是由各个目标文件经过链接而成,链接就是把各个目标文件的代码段、只读数据段、读写数据段经过了重新的排列组合。

    图片34.png

    需要注意的是未初始化数据段是怎么样的,在链接的过程中,链接器可以得到未初始化数据段的大小,它也是各个目标文件的各个未初始化数据段之和,但是这个段是不影响可执行程序大小的。在C语言使用的角度,读写数据段和未初始化数据段都是可读写的。实质上,在目标文件中未初始化数据段和读写数据段的区别也在于此,读写数据段占用目标文件的容量,而未初始化数据段只是一个标识,不需要占用实际的空间。

    在链接过程之前,各个源文件生成目标文件相互没有关系。在链接之后,各目标文件函数和变量可以相互调用和访问,从而被联系在一起。比如函数调用,链接过程就是要有函数调用的地方还需要找到真正的函数定义才可以完成链接,链接器会根据需要根据实际的情况修改编译器生成的机器代码,完成正确的跳转。全局变量的访问也是同理。

    再来了解下链接过程中常见的错误:

    1、符号未找到

    (1) 只要符号被声明,编译就可以通过,但是在链接过程中符号必须具有具体的实现才可以成功链接。

    (2) 由于数据仅能在文件内部使用(static),导致符号未定义错误。

    2、符号重定义

    (1) 在多个文件中定义全局的同名函数和变量(static的重名了是正确的)。

    (2) 在头文件中定义已经初始化数据,在头文件被多个文件包含的时候,将发生错误。同样在头文件中也不应该定义只读数据段的常量。

    再有在头文件中不应该使用静态的变量,无论它有没有初值,这样虽然不会引起链接错误,但是在各个源文件中各自产生变量,不但占用空间,而且在逻辑上是不对的,也违背了头文件的使用原则。

    C语言程序设计的角度,不应该在头文件中定义变量或者函数。对于函数,在头文件中只是声明,需要在源文件中定义;对于变量,无论何种性质,最好的方式是在C语言的源文件中定义,在头文件中使用extern声明使用。

    编译,链接后面就是执行了,后面我会跟大家再分享程序运行过程,这个其实都是C语言定的一些规则,只要守规则就会顺利完成想要实现的结果。

     

     


    收藏 0 回复 0 浏览 70
  • 一文带你读懂RFID

    大家好,我是张飞实战电子的黄忠老师,今天我们来了解下什么是RFID。

    RFID又称无线射频识别,通过无线电信号识别并读写特定目标数据,很容易就可完成识别与读写数据。RFID技术已经存在于我们生活中的方方面面,比如门禁卡、鸿蒙一碰传等等。       
           
    电子标签,比如说我们拿的卡门禁卡,进入天线磁场后,若接收到读写器发出的特殊射频信号,就能凭借感应电流所获得的能量发送出存储在芯片中的产品信息(无源标签,即不带电源),或者主动发送某一频率的信号(有源标签,即带电源),读写器读取信息并解码后,送至MCU系统进行有关数据处理。
                                   
    图片35.png
    关于RFID有几大产品:
     
    1. 无源RFID产品:
       
    无源RFID标签,被称为被动RFID标签这类产品需要近距离接触式读卡器来进行识别,比如房卡、银行卡等,当无源RFID标签靠近RFID读卡器时,无源RFID标签的天线将接收到的电磁波能量转化成电能,激活RFID标签中的芯片,并将RFID芯片中的数据发送出来。具有抗干扰能力,用户可自定义读写标准数据,在专门的应用系统效率更加快捷。无源RFID标签的性能受标签大小(影响到天线的参数),调制形式,电路Q值、器件功耗以及调制深度等影响。这类产品也是我们生活中比较常见,也是发展比较早的产品。价格相对有源RFID便宜,且使用寿命相对较长。其识别距离比有源系统要小,一般为几米到十几米,且需要较大的读卡器来发射功率。
    2. 有源RFID产品:
           
    有源电子标签是指标签工作的能量由电池提供,电池、内存与天线一起构成有源电子标签,不同于被动射频的激活方式,一直通过设定频段主动外发信息。这类型的产品则具有远距离自动识别的特性,所以相应地应用到一些大型环境下,比如智能停车场、智慧城市、智慧交通及物联网等领域。识别距离较长,通常可达几十米甚至上百米,缺点是成本高寿命有限,不易做成薄卡。       
    3. 半有源RFID产品:
         
     半有源电子标签顾名思义就是有源RFID产品和无源RFID产品的结合,内有电池,但电池只对标签内部电路供电,并不主动发射信号,其能量传递方式与无源系统类似,因此其工作寿命比一般有源系统标签要长许多。它结合二者的优点,只有在其进入低频读卡器的激活信号范围时,标签被激活后,才开始工作。解决了有源RFID产品和无源RFID产品不能解决的问题,比如门禁出入管理、区域定位管理及安防报警等方面的应用,近距离激活定位、远距离传输数据。
           
    RFID运用领域举例:
    1.仓库/运输/物资管理:给货品嵌入RFID芯片,管理人员就可以在系统迅速查询货品信息,防丢弃,提高货品交接速度,提高准确率,防止窜货和防伪。
    2.门禁/考勤
    3.固定资产管理:像图书馆、艺术馆及博物馆等资产庞大或者物品贵重的一些场所,当书籍或者贵重物品的存放信息有异常变动,就会第一时间在系统里提醒管理员,从而处理相关情况。


    收藏 0 回复 0 浏览 38
  • 在嵌入式系统中大小端和对齐问题

    大家好,我是张飞实战电子的黄忠老师,今天我们来讲解在嵌入式系统中大小端和对齐端的问题

    C语言是一种高级语言,在大多数情况下C语言的代码是和具体的处理器体系结构无关的。然而,在嵌入式系统的编程中,有可能涉及对内存的具体操作。在大小端和内存对齐问题上,C语言就不能屏蔽不同体系结构处理器的差别,也就是说同样的C语言代码在不同的体系结构的处理器上,有可能产生不同的结果。

    大小端问题又叫字节序的问题。在各种体系结构的处理器中,对多字节数据的内存操作有着不同的定义。处理器对内存数据的操作有读写两种,这就涉及处理器在读写一个多字节的内存的时候,高字节是在内存的高地址还是低地址。一般在32位或者16位的处理器中,都具有将32位数据和16位数据读写到内存中的指令,这时不同的大小端模式将有不同的结果。

    如果读写指令针对的数据长度和类型是一致的,无论数据在内存中存放的形式如何,处理器整体读写都没有问题。这种整内存协调的读写操作问题,一般不会涉及处理器的大小端。

    当处理器读写指令针对的数据长度不一致的时候就会涉及大小端的问题,例如:

    0x76543210整体放入内存,然后在内存的首地址用单字节读取的命令读出。

    如果不知道大小端模式的情况下,读取的值是多少你能确定吗?

    这时就涉及处理器是大端还是小端的问题。

    对于小端处理器,写内存的时候会将内存低地址处放入源数据的低字节,在内存的高地址处放入源数据的高字节;读内存的时候,将内存中低地址的数据就视为目标数据的低字节,对应的高地址数据是目标数据的高字节。

    对于大端处理器,跟小端就相反的。内存低地址存放数据的高字节,高地址存放数据的低字节。

    例如:数据0x76543210在内存中的大端或小端的存放形式如下:

    图片36.png

    上面的示例只是处理器自身读取和写入内存的情况,在更多的情况下,内存中的数据可能来自外界的输入,例如:来自网络的数据包;处理器在写内存的时候,这块内存也可能是给系统中别的设备使用的,例如:处理器写显示内存的情况。这时,就更需要注意处理器的大小端问题,只有大小端处理协调匹配,才能获得正确的结果。

    C语言中,使用指针就可以操作内存,指针的基本类型longshort分别代表了32位和16位的数据。使用16位或32位指针操作内存的时候,同样涉及内存的大小端问题。

    上面我们说了一下内存读写的模式不同,一个地址存的数据不同。

    接下来我们说一下内存对齐的问题,有人会说了内存对齐不对齐还需要你来管吗?这个在写程序的时候也是有讲究的,那么到底什么是内存对齐?为什么要有这个概念呢,我们来一起学习一下吧。

    内存对齐操作的含义是:对于一个4字节的数据,要求其内存是4字节对齐的(地址为4字节的整数倍)。32位对齐的含义是其内存的地址的最低位是:0x0,0x4,0x8,0xC

    16位对齐的含义是其内存的地址的最低位是:0x0,0x2,0x4,0x6,0x8,0xA,0xC,0xE

    显然,对于单字节的内存读写操作,没有内存对齐的问题。从处理器硬件的角度,处理器更适合处理对齐的内存操作。对于非对齐的内存操作,不同的处理器则有不同的结果。

    局部变量建立在栈空间上的,由编译器分配,一般保证它们都是对齐的。但是在程序中可能出现不对齐的内存操作。对于嵌入式系统中常用的ARM体系结构,并不支持不对齐的地址操作,当进行不对齐的地址访问的时候,处理器将引发异常。

    在嵌入式程序的编写过程中,更需要注意内存对齐的问题。对于内存操作,使用字节操作(8bit)不会有内存对齐的问题,但是效率比较低。在32位系统中,应该尽量使用32位的数据操作,但这将带来内存对齐的问题,因此需要根据系统的具体情况选择合适的内存操作。

    我们再来说说常纠结或者容易迷惑的结构体成员的对齐问题

    结构体是一个基本的语法单元。在32位系统中,编译器一般会对结构体的成员变量作一定的对齐处理。例如,在程序中定义如下结构体:

    typedef struct _S1

    {

    char m1;

    int m2;

    char m3;

    short m4;

    }S1;

    在结构体的定义上,结构体的大小应该是各个结构体成员的大小之和。但是,对于上面这个结构体S1,它的大小并不等于4个成员变量之和。在这种定义中,三个成员变量之和是1+4+2+2=8,但是结构体的大小并不是8字节。

    图片37.png

    编译器在处理结构体的时候,默认将结构体内部各个变量的内存都是对齐的,由此在结构体的内部可能出现一些空的字节。

    一般情况下,在结构体含有4字节长整型成员的时候,结构体的大小将是4字节的倍数。为了对齐可能需要在结构体的最后补充1~3个字节。

    如果结构体中含有2字节短整型成员的时候,结构体的大小将是2字节的倍数。为了对齐可能需要在结构体的最后补充一个字节。

    这个算字节数的一般出现在找工作中的笔试题的概率还是很高的,其实就是考察的对这个内存对齐的掌握。


    收藏 0 回复 0 浏览 157
  • 程序的优化技巧

    大家好,我是张飞实战电子黄忠老师,今天我们来讨论下程序的优化技巧!

    在嵌入式系统中由于资源比较有限,特别是内存资源,因此对程序运行的性能要求比较高。对执行效率高的程序段所占用的空间和运行效率进行全方位的优化,可以对程序运行的整体效率将产生可观的提升。

    1、循环缓冲区

    在一些嵌入式的系统中,常常需要开辟一块缓冲区保存数据。例如:对于数据采集系统,需要将一定时间段内的数据放入一个内存区域中。这个内存区域的放置方法是从低地址开始放置,如果放满了(到达了最高的地址),则需要从头部的低地址开始重新放置。这样的内存结构就组成了一个循环缓冲区。

    在一般的嵌入式处理器中没有硬件自动完成循环放置的功能,通常的做法是在程序的每次循环中都判断缓冲区是否放满了,显然这样的开销很大。

    如果要在程序中执行缓冲区类型的操作,这些操作一般需要占用一块连续的内存。在栈上分配的内存,一般只能在函数内部使用,函数退出的时候就会被释放,因此不适合作为缓冲区使用。而在堆上的内存和静态内存都可以作为缓冲区内存使用。

    我们举例来看下:

    #define  BUFFERSIZE 256

    int x[BUFFERSIZE];

    unsigned int k;

    unsigned int i;

    while(1)

    {

    k = i & (BUFFERSIZE-1);

    x[k] = ImputData();

    /*……*/

    i++;

    }

    从程序中可见,数组x[]是作为程序的缓冲区使用的,而由于开始并没有进行数组的初始化,x[]是一个建立在BSS段上的数组,其大小由BUFFERSIZE确定。

    我们看循环内的操作,可以完成自动循环的过程,这个例程中,当i增加到256的时候,k作为数组下标,又会返回为0i本身增加到最大值的时候也会变为0

    那么大家很容易看出来,由于不需要使用if做判断,可以节省几条程序指令的时间。对于这几条指令看似节省的时间不多,但是由于上述语句执行的频率非常高,所以这些时间的节省占程序总运行时间的权重还是比较大的。尤其对于实时采样处理问题,程序必须在指定时间内完成一系列的操作。所以对于执行效率比较高的指令,哪怕只节省一条指令,对运行效率的提高都是很有意义的。

    从以上的例子中可以看出,当进行程序优化的时候,不仅需要考虑程序段运行的绝对时间,还应该考虑程序段运行的频率。对于运行频率非常高的程序,对其进行优化会在很大的程度上提高系统的性能。

    2、查表法

    由于资源有限,程序的运行效率在嵌入式系统上比在PC上的程序开发更为重要。程序的运行速度和所占用的存储器空间这两个效率问题都是必须考虑的。嵌入式系统程序的运行速度与处理器频率有关系;而程序所能占用的存储器空间与ROMRAM的大小有关系。

    在当前的嵌入式系统中,程序的运行速度比程序所占用的存储器空间显得更重要,一是存储器方便扩展,二是存储器的容量是比较容易控制,程序运行占用的处理器时间比较难控制。

    在设计过程中,程序的容量和速度在很多时候是有些矛盾的,在程序中牺牲一定的存储容量换取程序的运行速度,这对于嵌入式系统来说是有一定好处的。典型的例子就是查表法。

    例如:在一个4位的二进制数中,确定有几位为1,也就是要统计0x0~0xf中的任何一个数,中间有几个1

    典型思路:         查表法:

    int getnumber(unsigned int a)     const int table[16] = {0,1,1,2,1,2,2,3,1,2,2,3,2,3,3,4};

    {           int getnumber(unsigned int a)

    unsigned int i=0;       {

    int num = 0;         return table[a&0x0f];

    unsigned int temp = a & (0xf);    }

    for(i =0;i < 4;i++)

    {

    if ((temp>>i)&0x01)

    num++;

    }

    return num;

    }

    典型的思路就是使用循环的方法让程序在这个4位的数中依次查找各个位是否为1,最后累加得出1的数目。那么在实现这个简单的功能的过程中,需要进行4次循环、4次判断,这是有一定开销的,占用了不少处理器的时间。从程序需要实现的功能考虑,输入是一个4位的数,范围是0x0~0xf,输出数的范围是0~4,这实际上是完成了一种映射功能,可以换成第二种查表法的思路,就是构造一个16个元素的数组,可以通过数组得到结果。实际上数组的下标就是输入的数值,而数组的元素就是输出的数值。

    那么很容易看出来,这种做法的优点是每个数值的获取非常快,代价则是增加了一个有16个元素的数组。数组是预先固化好的常量,而不是程序动态生成的,这种利用静态空间换取程序执行时间的方式转换后的程序执行效率非常高。如果把它应用在使用频率很高的程序中,就可以节省很多的系统开销。

    同样,大家可以考虑一下如果是查找8位数中的1的个数怎么做?16位呢?如果变通。

    3、针对循环执行效率的优化

    循环是C语言程序中的常用语法功能,由于循环执行的次数较多,占程序执行时间的权重大,所以对循环的优化是提高程序效率的关键点。

    例如,

    void change_list_value()

    {

    int i,count;

    POSITION pos;

    CPtrList* plist;

    plist = get_start(pos);

    for(i = 0; i < get_count(); i++)     count = get_count();      

    {          for(i = 0; i < count ; i++)

    plist = get_next(pos);     {}

    set_val (plist);

    }

    return 0;

    }

    上面这个循环代码左边是原始写法,右边是改进的。可以发现循环中执行的函数减少了,原来的get_count()函数从原来的内部转移到了循环外部,也就是说这个循环函数改进后只执行一次,如果这个链表中的元素有几千个至几万个,那么第一段代码比第二段代码多执行了几千条几万条的语句,这样会导致时间上巨大的开销。

    总结:在循环系统中,针对于循环条件,应该尽可能地使用临时变量来替代函数调用,这样可以在循环次数较多的情况下,减少大量不必要的函数调用。

    你有没有更好的优化技巧也分享出来啊~


    收藏 0 回复 0 浏览 99
×
黄忠