ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

嵌入式笔试高频考点全解析:从C语言到操作系统避坑指南

嵌入式笔试高频考点全解析:从C语言到操作系统避坑指南 2020年秋天我投了网易的嵌入式软件工程师提前批笔试是在线上完成的整个节奏比预想中快很多——投完简历没多久就收到了笔试邮件通过后直接进入面试环节没有正式批那么长的等待周期。当时我先后刷了三十多套嵌入式方向的笔试题网易这套是印象比较深的不是因为题目偏怪难而是它的考察面铺得很开从C语言底层细节到操作系统原理再到编程题几乎把嵌入式软件工程师需要掌握的通用基础全部筛了一遍。我身边不少同学也把提前批当作一次试水结果发现试水容易真正想通过还真得靠平时积累。这篇文章想把这套题背后反映出的能力要求、我在准备过程中整理的高频考点以及只有真正做题才会暴露的细节坑完整过一遍。不管是打算投嵌入式岗位校招的人还是已经收到笔试通知、想在节奏上提前规划的人应该都能从中找到能直接用的东西。1. 提前批笔试的整体画风不是难是广1.1 我遇到的大致题型分布网易这座体量的公司校招笔试一般都会走统一的在线笔试平台。嵌入式软件工程师提前批这套卷子整体结构可以分成三块选择题单选和多选都有覆盖C语言、操作系统、数据结构、计算机网络偶尔混进一两道逻辑推理或智力题。填空题或简答题常见的形式是补全代码片段、写出程序输出、解释某个概念有时还会给你一段代码让你指出问题。编程题一般是两到三题难度从送分题到区分度题逐渐递增。这里有一个很值得注意的特点嵌入式软件工程师笔试和普通后端软件工程师笔试最大的不同在于选择题和简答题里会出现计算机体系结构、寄存器操作、中断、编译链接这类偏底层的知识。也就是说如果只是按刷LeetCode的思路去准备选择题部分可能会被考得比较难受。1.2 提前批和正式批的时间节奏差异提前批的时间一般比正式批早一到两个月公司设置提前批的目的也很明确先把合适的候选人圈住。所以在流程上会压缩得很快笔试之后很快出结果紧接着就是面试。我那年身边有不少人同时投了提前批和正式批提前批过了之后正式批流程可以简化提前批挂了也可以继续走正式批所以就当多了一次全真模拟心理压力不用太大。不过正因为提前批节奏快卷子的风格会更偏向“一次筛到位”。我的整体感受是纯算法题的比例并没有想象中那么高但基础概念的覆盖范围更广有时候会在编程题之外夹一道方案设计类的简答题让你用文字描述某个嵌入式模块的实现思路。这种题不要求你写出完整工程但特别考察平时有没有真正搭过系统。1.3 三小时线上答题的时间分配在线笔试平台通常不支持本地IDE只能在一个网页编辑器里写代码对于习惯本地编译调试的人来说这是第一个需要适应的点。我当时给自己定的节奏是选择题和简答题控制在30到40分钟其余时间全部留给编程题。选择题里如果某道题卡了超过5分钟果断标记跳过拿不准的多选题可以先选一个最有把握的选项回头有时间再补。编程题有一个很实用的策略开考后不要顺着题号做先把所有题目快速浏览一遍判断每道题的难度和自己会不会做。我的习惯是先挑最有把握的那题做先把一血拿到再回头啃剩下的这样做能避免在最难的题上死磕半天导致最后连送分题都没时间写。提示草稿纸一定要利用好。在线编程没有本地断点也没有编译提示我一般先把核心算法的思路、边界条件、特殊用例画在草稿纸上再往编辑器里填出错概率会小很多。2. 嵌入式笔试躲不开的四块硬知识2.1 C语言底层细节指针、内存、结构体对齐C语言在嵌入式笔试里占的比重基本可以用“半壁江山”来形容。选择题、填空题、编程题都会从不同角度去考而最核心的考点基本集中在这几个方向上指针运算。p1到底移动几个字节取决于指针指向的类型。char*加1是移动1字节int*加1在32位平台上移动4字节。这个规则看起来简单但换到函数指针、数组指针、void**这些场景经常有人写错。数组和指针的关系。数组名在很多表达式中会退化成指向首元素的指针但a拿到的是整个数组的指针这两个类型不一样加1的结果也不一样。笔试特别喜欢拿这个做文章。内存分区。栈区、堆区、全局区、常量区、代码区每个区域存放什么变量生命周期分别是多少这是基础中的基础。动态内存分配。malloc、calloc、realloc、free的区别和注意事项内存泄漏是怎么产生的。在嵌入式场景里还会继续追问为什么MCU上经常不用malloc而是用静态分配或者内存池。这类题表面考API实际考的是内存管理思维。结构体对齐。结构体大小怎么算、为什么要对齐、怎么通过offsetof验证成员偏移。我备考时有一个体会笔试里问“进程的内存布局”时按代码段、数据段、BSS段、堆、栈这五个区域回答就够了但如果题目后面再追问“BSS段为什么不占可执行文件大小”或者“启动文件里是怎么初始化堆栈的”只背答案的人就会露馅。这个东西不是靠背出来的得真正打开启动文件和链接脚本看过之后才会在脑子里形成完整地图。2.2 数据结构与算法不用太深但基础必须扎实嵌入式岗位对算法的要求和算法工程师完全不同。它不要求你手写红黑树或者推导复杂的动态规划转移方程但非常看重常见数据结构的熟练度。我在笔试准备中统计过出现频率最高的是这些链表类操作反转链表、链表中环检测、两个链表的交点、有序链表合并。栈和队列用两个栈实现队列、循环队列判空判满、括号匹配、单调栈。二叉树前中后序遍历、层序遍历、最大深度、最近公共祖先。排序手写快排和归并排序以及它们的时间复杂度和稳定性。哈希表哈希冲突的解决方式链地址法和开放定址法的区别。这些内容看起来不难但最大的挑战是在白板环境下快速写出无bug的代码。我当时的练习方式是链表反转和二叉树遍历这类经典题至少手写二十遍不是为了背题而是让肌肉记忆代替现场思考。因为在线上笔试的环境里键盘反编译不方便脑内编译器的压力也大越是基础操作越要形成条件反射。2.3 操作系统与并发在嵌入式里是真刀真枪用的操作系统知识在嵌入式笔试里的考法和后端岗有明显的区别。后端岗偏向分布式、高并发而嵌入式笔试更传统考的是进程、线程、内存、调度这几块最核心的内容。常见的高频考点进程和线程的区别为什么嵌入式领域多用任务类似线程而少用进程。同步与互斥互斥锁、信号量、自旋锁、读写锁的适用场景。嵌入式里会特别关注优先级反转问题以及优先级继承怎么解决它。死锁的四个必要条件互斥、持有并等待、不可剥夺、循环等待以及如何破坏条件来预防死锁。调度算法先来先服务、短作业优先、时间片轮转、优先级抢占调度的适用场景。虚拟内存与分页页表、缺页中断、TLB的作用。进程间通信管道、共享内存、消息队列各自的适用场景。我复习操作系统时的一个心得是不要孤立地背定义而是把每个概念对应到一个具体的嵌入式场景。比如“互斥锁和信号量的区别”如果只背标准答案总觉得隔了一层但换到FreeRTOS的场景里就好理解了互斥锁通常带优先级继承机制用来保护共享资源计数型信号量更适合任务同步和资源计数。有了具体场景笔试简答题好答后面的技术面也有的聊。2.4 体系结构与外设寄存器、中断、字节序这一块是嵌入式笔试和其他软件岗拉开区分度的关键。选择题里经常出现寄存器操作、中断、字节序、编译链接相关的内容。需要掌握的核心概念包括寄存器操作怎么用C语言读写某个地址处的寄存器。标准写法是把地址强转成volatile指针然后解引用。为什么加volatile这是必考点。中断中断服务程序的编写约束为什么ISR里不能调用不可重入函数、不能做耗时操作、共享标志要加volatile。字节序大小端怎么判断网络字节序和主机字节序怎么转换。编译链接编译、汇编、链接、加载的完整过程符号表、重定位、静态链接和动态链接的区别。栈帧函数调用时栈帧是怎么建立的局部变量存在栈的什么位置函数指针是怎么调用的。字节序是我自己看书时一直比较模糊的概念后来写了一个联合体小实验才彻底搞清楚代码如下#include stdio.h #include stdint.h typedef union { uint16_t word; uint8_t bytes[2]; } endian_test_t; int main(void) { endian_test_t t; t.word 0x1234; if (t.bytes[0] 0x12) { printf(Big-endian\n); } else if (t.bytes[0] 0x34) { printf(Little-endian\n); } return 0; }这类十几行的小实验比对着书背十遍结论都管用。到考场上遇到大小端、位域分配方向这类题时会明显觉得底气更足。3. 编程题拆解从读题到避坑再到AC3.1 数组与字符串题边界条件才是主战场嵌入式笔试的编程题通常不会出特别偏的题目据我刷过的几十套题来看数组和字符串相关题出现频率最高。常见的有数组去重、排序后找第K大的元素连续子数组最大和字符串翻转、字符串中单词逆序、判断回文大数相加找数组里唯一出现奇数次的数字这类题的考点往往不在核心思路而在边界条件和复杂度控制。比如“找数组里唯一出现奇数次的数字”如果没接触过异或运算第一反应可能是用哈希表统计但最优解是用异或把出现偶数次的数字全部抵消时间O(n)、空间O(1)。又比如字符串处理最容易翻车的不是主体逻辑而是空字符串、只含一个字符、连续空格这类边界输入。我备考时养成了一个习惯每次写完代码先在草稿纸上列出三四个边界用例手动走一遍再提交。在线上编辑器没有断点的情况下这一步基本等于自测能拦截掉一半以上的低级bug。3.2 链表操作题指针关系要形成条件反射链表可以说是嵌入式软件工程师笔试和面试的共同常客。这背后的逻辑很明显链表操作对手写代码的熟练度要求很高能从指针操作中直接看出写C语言的基本功。反转链表是最典型的代表迭代写法如下struct ListNode* reverseList(struct ListNode* head) { struct ListNode* prev NULL; struct ListNode* curr head; while (curr ! NULL) { struct ListNode* next curr-next; curr-next prev; prev curr; curr next; } return prev; }代码不长核心就是“先保存next再改指向再滑动”这三步。但线上笔试环境里没有本地调试指针关系一紧张就容易弄混写完之后一定要在草稿纸上用两个节点、三个节点的例子各走一遍确认没有断链再提交。链表题还有一个容易忽略的点处理头节点变化时有时用虚拟头节点dummy node会更省心比如删除链表倒数第N个节点用虚拟头节点能避免讨论头节点被删的特例。3.3 嵌入式特色编程题位操作与寄存器读写和纯软件岗位相比嵌入式笔试还有一个特色就是编程题里偶尔会混进和硬件直接相关的题目。大概有这么几类给一个整数实现对其第n位的置位、清零、翻转、判断。给定设备寄存器基地址和字段掩码写代码实现对某个寄存器字段的修改。写一个简单状态机解析通信协议帧。实现一个环形缓冲区或者固定容量的队列。这类题目难度本身不高但有一个细节非常重要涉及寄存器地址时指针一定要加volatile。我笔试完复盘过一道题用结构体指针直接映射外设寄存器地址因为结构体默认对齐和寄存器偏移不一致读出来的数据完全是错的。后来加上__attribute__((packed))再补上类型检查问题才解决。这些细节笔试里可能不会直接戳穿但作为嵌入式软件工程师这正是你能不能真正在硬件上跑通代码的分水岭。4. 容易丢分的细节我总结的十个细节坑4.1 volatile、const、static的正确理解这三个关键字在嵌入式笔试里的出镜率极高而且经常组合起来考。先说各自的语义volatile——告诉编译器这个变量的值可能在编译器无法感知的地方被修改所以每次使用都必须从内存重新读取不能优化到寄存器里。中断处理函数和主循环共享的标志位、寄存器映射地址、多线程共享变量都是典型场景。const——表示这个值不能被修改。const int *p和int *const p的区别本身就能出一道多选。static——修饰局部变量时改变其存储位置和生命周期修饰全局变量或函数时限制其作用域。组合起来就会出现像volatile const int *p这样让人犯迷糊的题目。我的理解方法是拆开看指针指向的内容不能被程序修改但可能被硬件修改所以volatile和const并不冲突只读状态寄存器就是典型的volatile const场景。4.2 结构体对齐算大小不能靠直觉结构体大小是我见过选择题翻车率最高的考点之一。很多人第一反应是把所有成员大小加起来但实际还要对齐。比如struct Example { char a; // 1字节 int b; // 4字节 char c; // 1字节 };在常见32位平台上a占1字节后编译器会填充3个字节让b对齐到4字节地址c占1字节后整个结构体再填充到4的倍数最终结果是12字节而不是6。这题已经算是温和的真正有区分度的是把它和数组、位域、嵌套结构体结合起来的题计算时要逐层分析对齐规则。4.3 有符号与无符号数的隐式转换C语言里有符号数和无符号数出现在同一个表达式里时有符号整数会被隐式转换为无符号整数。这个规则几乎所有写过C语言的人都知道但实际用起来还是会踩坑。最典型的是这段循环unsigned int n 10; for (int i n - 1; i 0; i--) { // 问题很大 }这里i和n-1比较时i会被转换成无符号数。当i减到-1时它会被看作一个巨大的无符号数循环条件仍然成立结果就是死循环。这种bug在线上笔试里几乎无法调试一旦提交就浪费一次机会。我后来的习惯是凡是写循环边界先看一眼变量类型再决定要不要用 0这种判断。4.4 位运算优先级与逻辑运算符位运算的优先级比大多数人的直觉要低。的优先级低于所以if (a b 0)实际上会被解析成if (a (b 0))和期待完全不一样。同理|的优先级也低于如果混着用很危险。最稳妥的办法是只要涉及位运算一律显式加括号。不要嫌括号啰嗦它能把你的真实意图清晰地告诉读者和编译器。4.5 中断处理函数里的注意事项简答题经常让你判断“以下哪些操作不适合在ISR中执行”。核心原则很简单ISR要短小精悍。不能在ISR里做阻塞等待不能调用不可重入的或耗时长的函数比如printf和malloc也要避免访问可能产生阻塞的外设。如果要和主循环共享数据共享变量要加volatile必要时还要考虑临界区保护。把“ISR要尽量短小”这个原则理解到位相关题目基本不会做错。4.6 字节序判定与联合体陷阱字节序相关的考题有两种一种是让你判断某段代码的输出另一种是让你判断本机是大端还是小端。我之前用联合体写过一段判断字节序的代码上面第2节的例子里已经展示过。这里想提醒的是字节序不仅影响数据存储在网络通信、协议解析、固件升级、串口收发这些场景里都会有影响。笔试里常问“收到一个字节流后怎么把它组装成多字节整数”这背后就是在考字节序意识。4.7 malloc成功不等于拿到了可用的内存malloc失败返回NULL是常识但实际代码里很多人拿到返回值后直接解引用没有判空。嵌入式笔试考这个点经常放在简答题里问“为什么嵌入式环境中往往不用动态内存分配”标准答案里除了实时性和内存碎片还有一个重要原因就是失败处理麻烦。笔试的时候遇到malloc相关代码不管多简单坚持先判空再使用这属于一种职业习惯。4.8 strcpy换成strncpy之后仍然有坑strcpy危险很多人知道用strncpy替代但strncpy的边界行为也容易忽略。它对源字符串长度超过n的情况不会自动加\0所以目标缓冲区不一定是合法的字符串。笔试题如果让你指出代码问题这里是一个高频埋点。正确做法是主动在末尾置\0比如strncpy(dst, src, sizeof(dst) - 1); dst[sizeof(dst) - 1] \0;还有更直接的方式是用safe的字符串复制函数但笔试环境不一定支持所以把strncpy的边界细节记牢是必须的。4.9 野指针的成因和指针初始化的习惯野指针的出现本质是“指针指向的内存已经失效但指针的值还在”。常见成因有局部指针未初始化就使用指针指向栈上变量函数返回后继续使用free之后没有把指针置为NULL然后再次解引用。笔试如果考阅读代码经常在函数返回后访问局部变量地址这种场景设陷阱。我的习惯是指针定义时就初始化为NULL或者立即赋值free之后马上置NULL这样即使后续出错也能在排查时快速定位。4.10 宏定义里的括号问题宏是C语言笔试的常客因为宏定义只是简单文本替换不会帮你做运算优先级。经典的#define SQUARE(x) x*x在SQUARE(ab)里就会算出完全错误的结果。正确写法是#define SQUARE(x) ((x) * (x))多一层括号不是多余是在保护使用者。类似的还有MIN宏、MAX宏都要把参数和整个表达式分别括起来。不过笔试到后面也常考“宏和函数的区别”回答要点包括宏没有类型检查、容易产生副作用、不能调试、可能增加代码体积而函数没有这些问题。5. 笔试结束后的复盘与面试衔接5.1 如何回溯答错的知识点线上笔试结束之后很多平台不会立刻显示正确答案等结果的时间可能是几天甚至一两周。这个期间别干等我建议趁答题记忆还新鲜把每道题所涉及的知识点写下来尤其是那些标记跳过、或者交卷时心里没底的题。等到笔试题解析出来以后逐题对照重点不是记住正确答案而是想清楚“我当时为什么会在这里卡住”。我复盘时会做一个很简单的归类把错题归到具体知识域比如“这题考的是结构体对齐”“这题考的是信号量互斥”“这题考的是字节序”。这样归类完之后薄弱点会非常清楚地浮出来后续复习就有针对性不需要闷着头把整本C语言从头看一遍。5.2 把笔试考点变成面试问题清单笔试是筛选已知信息面试是考察理解深度。最典型的就是笔试里考“寄存器地址为什么强转成volatile指针”面试可能直接让你讲讲“编译器优化对寄存器操作的影响”。如果准备笔试只停留在背答案面试大概率会被三连追问问倒。我的方法是笔试复习时对每一个高频考点额外问自己一层“为什么”。比如信号量为什么能解决同步问题它的原子操作是怎么实现的volatile能不能解决所有并发问题为什么不能在每个考点下面写两行理解层面的笔记等笔试通过后这份笔记就成了现成的面试复习材料。5.3 提前批失利不意味着结束提前批相对正式批来说周期短、流程快但也正因为如此它的名额通常不会很多。如果你参加了提前批笔试最终没有通过不要直接否定自己。我身边有同学提前批笔试挂了后来在正式批里一路走到终面并拿到意向书因为正式批的岗位方向、部门需求都可能不同准备的充分度也不一样。这类提前批笔试如果只拿来当“测一测自己能考多少分”其实是一种浪费。我的想法是把每一次笔试都当成一次可以回溯的练习答完题之后记录问题、复盘错点、补齐知识漏洞那这几个小时的考试价值就远超一场筛选本身。真正拉开差距的往往不是你多刷了多少题而是你能不能从每一次实战中把自己补得更完整。
返回列表