北京零零科技 嵌入式软件开发一面
1. 先做个自我介绍吧
参考答案
您好,我是XXX,主要从事嵌入式软件开发。技术栈方面,熟练掌握C/C++编程,有单片机裸机开发和Linux系统开发经验。
在单片机方面,做过基于ARM Cortex-M内核的项目,涉及串口通信、ADC采集、PWM控制、定时器等外设驱动开发。
Linux方面,熟悉应用层编程,包括多进程、多线程、网络编程等。
最近在做的项目涉及多核处理器的核间通信和DMA数据传输优化。对嵌入式系统的实时性和资源优化比较有心得。
2. 看你简历上有单片机和Linux开发经验,具体做过哪些?驱动层有接触吗?
参考答案
单片机方面:
主要使用过STM32和Infineon TRAVEO系列MCU,做过完整的外设驱动开发,包括:
- GPIO控制(按键输入、LED输出)
- UART串口通信(包括DMA方式收发)
- ADC多通道采集
- PWM波形生成(电机控制)
- 定时器中断
- 外部中断
- Flash读写操作
Linux方面:
主要在应用层开发,做过:
- 多进程/多线程程序设计
- 进程间通信(管道、共享内存、消息队列)
- Socket网络编程(TCP/UDP服务器客户端)
- 文件IO操作
驱动层:
有一定了解,看过字符设备驱动的代码,了解驱动的基本框架(init、exit、open、read、write、ioctl等接口),但实际项目中主要还是调用现成的驱动接口进行应用开发。
3. 内存管理这块,进程的虚拟地址空间是怎么划分的?
参考答案
进程的虚拟地址空间从低到高主要分为以下几个区域:
1. 代码段(.text)
- 存放程序的可执行指令
- 只读属性,防止程序被意外修改
- 多个进程可以共享同一份代码段
2. 数据段(.data)
- 存放已初始化的全局变量和静态变量
- 可读可写
3. BSS段(.bss)
- 存放未初始化的全局变量和静态变量
- 程序加载时自动清零
- 不占用可执行文件空间,只记录大小
4. 堆(Heap)
- 动态内存分配区域
- 通过malloc/free或new/delete管理
- 从低地址向高地址增长
- 程序员手动管理,容易出现内存泄漏
5. 内存映射区(mmap)
- 动态链接库加载区域
- 文件映射区域
6. 栈(Stack)
- 存放局部变量、函数参数、返回地址
- 从高地址向低地址增长
- 自动分配和释放
- 大小有限制(通常几MB)
7. 内核空间
- 位于最高地址
- 用户态程序不能直接访问
典型的内存布局:
0xFFFFFFFF ┌─────────────┐
│ 内核空间 │
├─────────────┤
│ 栈 ↓ │
│ │
│ ... │
│ │
│ 堆 ↑ │
├─────────────┤
│ BSS段 │
├─────────────┤
│ 数据段 │
├─────────────┤
│ 代码段 │
0x00000000 └─────────────┘
4. 那BSS段的变量有什么特点?初始值是多少?
参考答案
BSS段(Block Started by Symbol)存放未初始化的全局变量和静态变量,有以下特点:
1. 自动初始化为0
- 所有未显式初始化的全局变量和静态变量,系统会自动将其初始化为0
- 这是确定性的行为,不是随机值
2. 不占用磁盘空间
- 在可执行文件中,BSS段只记录大小信息,不实际存储数据
- 节省可执行文件的体积
3. 运行时分配
- 程序加载到内存时,操作系统为BSS段分配空间并清零
示例:
int g_uninit; // BSS段,值为0
static int s_uninit; // BSS段,值为0
int g_init = 100; // 数据段,值为100
static int s_init = 200; // 数据段,值为200
void func() {
static int local_uninit; // BSS段,值为0
int auto_var; // 栈上,值未定义(随机)
}
注意区别:
- 全局/静态变量未初始化 → BSS段 → 自动为0
- 局部变量未初始化 → 栈上 → 值不确定(随机垃圾值)
5. 动态内存分配和释放你了解吗?用什么函数?释放的是哪块内存?
参考答案
C语言动态内存管理
分配函数:
malloc(size):分配指定大小内存,内容未初始化calloc(n, size):分配n个size大小的内存,并清零realloc(ptr, new_size):重新调整已分配内存的大小
释放函数:
free(ptr):释放之前分配的内存
C++动态内存管理
- 分配:
new/new[] - 释放:
delete/delete[]
释放的内存位置
动态分配和释放的内存都在堆区(Heap)。调用free或delete后,内存归还给堆管理器,可以被后续的malloc/new重用。
示例:
// C语言
int *p = (int*)malloc(sizeof(int) * 10); // 堆上分配
if (p != NULL) {
// 使用内存
free(p); // 释放堆内存
p = NULL; // 避免野指针
}
// C++
int *arr = new int[10]; // 堆上分配
delete[] arr; // 释放堆内存
arr = nullptr;
常见问题
- 内存泄漏:分配后忘记释放
- 重复释放:对同一指针多次free/delete
- 野指针:释放后继续使用
- 不匹配释放:malloc配delete,或new配free
6. 说说你对C语言指针的理解?
参考答案
指针是C语言最核心也最强大的特性,我的理解主要有以下几点:
1. 本质
指针是一个变量,它存储的是另一个变量的内存地址。通过地址可以间接访问和修改数据。
2. 基本操作
int a = 10; int *p = &a; // 取地址:&a得到a的地址 *p = 20; // 解引用:*p访问p指向的内存,修改a的值
3. 指针的优势
- 高效传参:传递大结构体时,传指针避免拷贝开销
- 动态内存:通过指针操作堆上分配的内存
- 直接操作硬件:嵌入式开发中通过指针访问寄存器
volatile uint32_t *reg = (uint32_t*)0x40020000; *reg |= 0x01; // 设置寄存器某一位
- 实现复杂数据结构:链表、树等都依赖指针
4. 指针类型
- 普通指针:
int *p - 指针数组:
int *arr[10]- 10个int指针的数组 - 数组指针:
int (*p)[10]- 指向含10个int的数组的指针 - 函数指针:
void (*func)(int)- 指向函数的指针 - 多级指针:
int **pp- 指向指针的指针
5. 注意事项
- 野指针:未初始化或已释放的指针
- 空指针:使用前要判断是否为NULL
- 指针越界:访问超出分配范围的内存
- 类型匹配:指针类型要与指向的数据类型匹配
6. 嵌入式应用
在单片机开发中,指针用于:
- 寄存器操作
- DMA缓冲区管理
- 中断向量表
- 内存映射外设访问
7. 函数指针和普通指针的大小一样吗?为什么?
参考答案
在同一个平台上,函数指针和普通数据指针的大小是相同的。
原因
无论是函数指针还是数据指针,它们的本质都是存储一个内存地址。地址的大小由系统的地址总线宽度决定,与指向的内容类型无关。
具体大小
- 32位系统:所有指针都是4字节
- 64位系统:所有指针都是8字节
验证代码
#include <stdio.h>
void test_func(int x) {
printf("x = %d\n", x);
}
int main() {
int a = 10;
int *p1 = &a; // 数据指针
void (*p2)(int) = test_func; // 函数指针
char *p3 = "hello"; // 字符指针
double *p4; // double指针
printf("int* size: %zu\n", sizeof(p1));
printf("func* size: %zu\n", sizeof(p2));
printf("char* size: %zu\n", sizeof(p3));
printf("double* size: %zu\n", sizeof(p4));
// 在64位系统上都输出8,32位系统上都输出4
return 0;
}
注意
虽然指针大小相同,但指向的内容大小可能不同:
sizeof(int*) // 指针本身:4或8字节 sizeof(*p) // 指向的int:4字节 sizeof(double*) // 指针本身:4或8字节 sizeof(*p) // 指向的double:8字节
8. C语言如何模拟实现面向对象中的成员方法?
参考答案
C语言可以通过结构体+函数指针的方式模拟面向对象的成员方法,这种设计模式在Linux内核和很多C语言框架中广泛使用。
实现方式
#include <stdio.h>
#include <stdlib.h>
// 定义"类"结构体
typedef struct Motor {
// 数据成员
int speed;
int direction;
// 方法成员(函数指针)
void (*start)(struct Motor* self);
void (*stop)(struct Motor* self);
void (*setSpeed)(struct Motor* self, int speed);
void (*getStatus)(struct Motor* self);
} Motor;
// 实现"成员方法"
void
剩余60%内容,订阅专栏后可继续查看/也可单篇购买
这是一个全面的嵌入式面试专栏。主要内容将包括:操作系统(进程管理、内存管理、文件系统等)、嵌入式系统(启动流程、驱动开发、中断管理等)、网络通信(TCP/IP协议栈、Socket编程等)、开发工具(交叉编译、调试工具等)以及实际项目经验分享。专栏将采用理论结合实践的方式,每个知识点都会附带相关的面试真题和答案解析。

查看4道真题和解析