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

本文介绍了嵌入式系统中关于大小端的问题,包括小端和大端处理器的内存读写方式,并通过例子说明了内存对齐在结构体存储中的规则。此外,还探讨了函数跳转在嵌入式应用中的实现,尤其是如何通过函数指针进行绝对地址跳转。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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;
}
这里的返回值一般表示程序出错。

<think>好的,我现在需要处理用户关于嵌入式开发面试常见问题及其答案的查询。首先,用户已经提供了三个引用内容,我需要仔细阅读这些引用,提取相关信息,然后组织成一个结构清晰的回答。 首先,查看引用[1],里面提到嵌入式软件面试的问题主要围绕基础知识、项目经验、问题解决能力和对嵌入式系统的理解。这里特别提到C/C++的区别,但用户可能需要更全面的问题列表。引用[2]讨论了结构体的内存对齐,这在嵌入式系统中很重要,因为内存管理很关键。引用[3]则包含多个具体问题,如STM32时钟系统、堆栈区别、I2C协议、中断处理流程等,这些都是嵌入式面试中的经典问题。 接下来,我需要将这些信息分类整理。常见的问题通常分为基础知识、硬件相关、操作系统/驱动、通信协议、项目经验等几个大类。引用中的内容覆盖了C语言、单片机、通信协议(如IIC)、内存管理(堆栈、结构体内存对齐)以及中断处理。需要将这些内容分门别类,确保回答结构清晰。 然后,我需要考虑用户可能的深层需求。用户可能希望准备面试,因此答案不仅要列出问题,还需要简洁明了的解答,帮助快速复习。同时,可能需要补充一些引用中没有但常见的问题,比如实时操作系统(RTOS)、指针的使用、volatile关键字等,这些在嵌入式开发中也很重要。 还要注意引用中的细节,比如结构体内存对齐的三个条件,堆和栈的区别,I2C的工作流程等,这些都需要准确无误地呈现。同时,要确保使用正确的中文术语,比如“中断处理流程”而不是“中断处理过程”。 最后,生成相关问题部分,应该涵盖用户可能感兴趣的扩展方向,比如优化方法、实际应用场景、常见错误处理等。例如,关于内存泄漏的排查、RTOS的任务调度机制、SPI与I2C的区别等,这些都是嵌入式面试中可能被追问的问题。 总结步骤: 1. 分类整理引用中的问题,补充常见问题。 2. 确保每个问题有简明答案,引用相关来源。 3. 结构清晰,分点列出,便于阅读。 4. 生成相关问题,帮助用户进一步准备。</think># 嵌入式开发面试常见问题及参考答案 ## 一、基础知识类 1. **C语言结构体内存对齐规则** 结构体成员地址偏移量需满足:首成员与结构体首地址对齐;每个成员地址偏移量是其大小的整数倍;结构体总大小是其最大成员大小的整数倍[^2]。 2. **堆与栈的区别** - 存储内容:栈存局部变量/函数参数,堆存动态分配内存 - 管理方式:栈由编译器自动管理,堆需手动申请/释放 - 空间大小:栈空间固定较小,堆空间灵活较大 - 分配效率:栈分配更快,堆涉及系统调用[^3] 3. **volatile关键字的作用** 防止编译器优化,强制从内存读取变量值,常用于多线程/中断环境下的共享变量。 --- ## 二、硬件与单片机 4. **STM32时钟源类型** 包含5种:HSI(内部高速)、HSE(外部高速)、LSI(内部低速)、LSE(外部低速)、PLL(锁相环倍频)。 5. **51单片机中断处理流程** 1. 中断响应:硬件生成LCALL指令保存断点 2. 跳转执行:PC指向中断向量表入口 3. 中断服务:执行用户定义的中断函数 4. 中断返回:RETI指令恢复断点并清除优先级标志 --- ## 三、通信协议 6. **I²C协议核心特性** - 半双工串行总线,需SCL(时钟)和SDA(数据)线 - 传输单位:8位数据+1位应答(ACK/NACK) - 必须由起始条件(START)开始,终止条件(STOP)结束 - 支持多主从架构,通过地址寻址[^3] 7. **SPI与I²C主要区别** | 特性 | SPI | I²C | |------------|----------------|------------------| | 线路数量 | 4线(SCK, MOSI, MISO, SS) | 2线(SCL, SDA) | | 传输速率 | 更高(可达MHz级) | 较低(通常400kHz-5MHz) | | 拓扑结构 | 主从式 | 多主从 | --- ## 四、系统与优化 8. **嵌入式系统内存泄漏排查方法** - 使用静态分析工具(如Coverity) - 动态监测工具(Valgrind) - 手动记录malloc/free调用次数 - 重点检查中断服务程序中的动态分配 9. **RTOS任务调度机制** 典型采用优先级抢占式调度,通过就绪队列管理任务状态,支持信号量/消息队列等同步机制。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值