大小端,内对齐及函数跳转在嵌入式中的应用

来源:互联网 发布:js横向时间轴滑动插件 编辑:程序博客网 时间:2024/06/03 05:05

1)大小端的问题(只是针对多字节,对于单字节,是不存在大小端问题的): 

我们将一个4字节的数据0x7654321放入内存,然后将其读出也是0x7654321,在这个过程中一般是不会出问题的,这是因为对于一种处理器,如果读写指令针对的数据长度和类型是一致的,无论数据在内存中存放的形式如何,处理器整体读写都没有问题。

问题在于,将0x7654321整体写入内存,然后在内存的首地址用单字节读取的命令的读出,这个时候可能就有问题了,就涉及到大小端的问题:

小端处理器:

写内存:将内存低地址放入源数据的低字节,内存高地址放入源数据的高字节
读内存:将内存低地址视为目的数据的低字节,内存高地址视为目的数据的高字节

大端处理器:

写内存:将内存低地址放入源数据的高字节,内存高地址放入源数据的低字节
读内存:将内存低地址视为目的数据的高字节,内存高地址视为目的数据的低字节

存放如下对比(源数据为:0x1234567):


下面给出一个例子:

typedef struct _Byte4{    unsigned char Byte0;    unsigned char Byte1;    unsigned char Byte2;    unsigned char Byte3;}Byte4 ;typedef union _Data32{    unsigned long data;    Byte4 dataByte;}Data32; int main(){    Data32 a;    a.data = 0x11223344;    printf("Databyte(0, 1, 2 ,3 ):(%x, %x, %x, %x)\n",        a.dataByte.Byte0, a.dataByte.Byte1,        a.dataByte.Byte2, a.dataBtye.Byte3);}
对于这个问题,如果是小端系统,结果是:
Databyte(0, 1, 2, 3):(44, 33, 22, 11)
如果是大端系统,结果是
Databyte(0, 1, 2, 3):(11, 22, 33 ,44)
这里顺便给出测试处理器大小端的简单程序:

int testendian(){    unsigned int x=1;    if(1 == *(unsigned char *)&x)          printf("Little Endia\n");    else          printf("Big Endia\n");    return (1 == *(unsigned char *)&x);}
2)内存对齐问题(这里默认是小端系统)
   内存对齐的含义是:对于一个4字节的数据,要求其内存是4字节对齐的(即存储起始地址为4字节的整数倍)。这就是说,32位对齐的含义是其内存的地址的最低位为:0x0, 0x4, 0x8, 0xc;16位对齐(即2字节对齐,存储起始地址为2字节的整数倍)的含义是内存的地址的最低位为:0x0, 0x2, 0x4, 0x6, 0x8, 0xA, 0xC, 0xE.而对于单字节数据是不存在内存对齐问题。一般情况下,编译器在变量的栈上进行内存分配的时候,都能保证根据其自己的数据类型做到内存对齐,不懂,看下边的例子:
char v1;    short v2;    short v3;    long v4;
假设:v1的地址为0xbffff7f7
那么上述四个变量的地址分别如下:0xbffff7f7    0xbffff7f4    0xbffff7f2    0xbffff7ec结果是怎么来的呢,看下边(在这里只考虑最后变化的两位):
注意:计算机中的内存是以字节为单位的连续的存储空间,每个字节都有一个唯一的编号,这个编号就称为内存地址;因为内存的存储空间是连续的,所以,内存字节的地址编号也是连续的:


解释如下:我们已经知道,v1的地址是0xf7。如果常规理解,v2是short,占两个字节,那么应该从0xf5开始,占据0xf5和0xf6两个字节,即地址是0xf5。但由于0xf5不是两字节对齐(见上边),所以要继续向低地址移动到0xf4(它是两字节对齐的),所以v2的地址是0xf4,占据0xf4,0xf5两个字节,v3同理地址是0xf2。对于v4而言,从0xf1开始左移4个字节到0xee,但它不是4字节对齐的,所以继续左移到0xec,占据0xec,0xed,0xee,0xef四个字节,完成存储。
既然说到字节对齐,常常让人忘不了,sizeof对结构体的操作,一般而言,需要满足3个准则:
A.结构体变量的首地址能够被最宽的基本类型成员的大小所整除
B.结构体每个成员相对于结构体首地址的偏移量都是成员大小的整数倍,如有需要编译器会在成员之间加上填充字节。
C.结构体的总大小为结构体最宽的基本类型成员的整数倍,如有需要编译器会在最后一个成员后加上一个填充字节。
如:
struct S{
    char c1;
    int i;
    char c2;
};
c1的偏移量为0,i的偏移量为4,c1与i之间便需要填充3个字节,c2偏移量为8,加起来就是1+3+4+1等于9个字节,由于这里最宽的基本类型是int,大小为4字节,再补3字节凑成4的倍数,一共是12字节。
   其实上面的关键在于,在编译器处理结构体的时候,默认将使结构体内部各个变量的内存都是对齐的,由此在结构体的内部可能出现一些位的空字节。
3)函数调整在嵌入式的应用
C语言中程序的一种最基本的跳转方式就是函数调用。经过编译后,在处理器的角度来看就是跳转到了函数的起始地址处,开始执行那个相应位置的代码。这就是告诉我们,在嵌入式系统中,可以使用绝对地址的跳转。这时程序跳转不是去调用一个函数,而是将程序运行的地址(即程序计数器PC)置为绝对地址。然而在各种处理器的汇编语言中,有绝对跳转的指令,但C语言是高级语言,要完成这种绝对地址的跳转,需要通过函数指针这种特殊的形式来实现。
利用函数指针来实现绝对地址跳转。如下:

typedef void (* FUN_T)(void);void display(void){    ....}int main(void){    FUN_T pf=display;    pf();         return 0;}运行到pf()时,调用函数指针,将实现跳转到display函数处(地址处)。
通过上面我们易想到把display换成绝对地址,不是就跳到了绝对地址那里吗?不错,如下:

int main(void){    FUN_T pf=(FUN_T)0x400000000;  //这里将函数指针设置为绝对地址。    pf();   //调用函数指针,实现跳转到  0x400000000    return 0;}上面的代码可以不用类型定义,等价于如下代码:void (*des)(void);des = (void (*)(void))0x40000000;des();
上面的跳转中,没有参数,也没有返回值,其实实际上是可以传递到跳转处参数,以及获取返回值:

typedef int (* FUN_T)(int, int);int main(){    int ret;    FUN_T pf = (FUN_T)0x40000000;  ret = (*pf)(0x10, 0x12);  return 0;}
这里的返回值一般表示程序出错。

原创粉丝点击