ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

奇安信软件笔试解析:C/C++内存与安全底层考点全拆解

奇安信软件笔试解析:C/C++内存与安全底层考点全拆解 2020年秋招那阵子我刷到奇安信这套软件开发方向的笔试卷做完之后最大的感受是不按套路出牌但仔细一品又全在套路里。它不像很多互联网大厂那样堆一堆“背多分”的八股而是把安全厂商对软件开发工程师的真实要求摊开给你看——C/C功底、内存布局的理解、网络协议的细节、操作系统的并发模型甚至还有手撕代码的硬功夫。如果你是准备安全方向或系统软件方向校招的同学这套卷子很有参考价值如果你只是单纯想查漏补缺看看自己在底层基础上有哪些盲区同样值得花两个小时认真过一遍。我把整套卷子的考点、答题思路和容易踩的坑整理出来结合我自己的做题和复盘经历一篇一篇拆开讲。1. 开篇先看清楚这套卷子的整体画像与考点权重先说结论这套卷子整体难度对刚从实验室出来、主要用Java或者Python做业务开发的同学来说是偏高的。它的默认语言倾向是C/C即使是非语言题也经常在题干里夹带指针、内存、字节序这些概念。并不是说不会C就完全做不了但安全软件公司对“底层敏感度”的考察意向非常明显这本身就是岗位要求决定的。从考点分布上看我大致归成了五类考点模块涉及内容大约分值占比主观感受C/C语言与内存指针运算、数组越界、内存布局、字节序、结构体对齐25%考得细容易在“自以为会”的地方翻车计算机网络TCP/UDP细节、HTTP/HTTPS、加密与数字签名、常见攻击原理20%注重协议交互过程不是简单背概念操作系统与并发进程线程区别、锁的实现、死锁条件、调度策略20%喜欢结合场景分析不是直接问定义数据结构与算法链表操作、排序变体、Top K问题、动态规划基础25%手写代码题占一定比重要求一次性通过安全基础与综合加解密基础、常见Web漏洞、安全开发流程10%安全厂商特色多少有点护城河性质这个权重分布和一般互联网公司后端岗的卷子差异很明显。常规后端卷子往往把网络协议、数据库、缓存三件套作为大头而奇安信的卷子几乎没有数据库题倒是在C语言指针和进程同步上反复做文章。所以如果你计划投安全厂商的软件开发岗复习重心要偏向系统底层这不是临时抱佛脚能补上来的需要平时写代码时就有意识。整套题还有一个特点多选题占比不低而且很多题是“选出正确的/错误的”这种反向设问。这种题目的难点不在知识本身而在读题。我第一遍做的时候有两道题明明会却因为看漏了“不正确”两个字选反了白丢分。后面细讲具体题目时我会把这些读题陷阱单独指出来。2. C/C与内存管理题高手和普通选手的分水岭2.1 一道“简单”的指针题为什么错了一大批人卷子里有一道典型的指针运算题代码大致是这样#include stdio.h int main() { int a[4] {10, 20, 30, 40}; int *p a; printf(%d\n, *(p)); printf(%d\n, *p 1); printf(%d\n, *(p 2)); return 0; }问三次输出分别是什么。这道题考的是两个最容易混的点*(p)是“先取值再自增”还是“先自增再取值”*p 1和*(p 1)到底差在哪先说答案第一次输出10因为*(p)先用p当前指向的a[0]打印10之后p偏移到a[1]。第二次输出21此时p指向a[1]即20*p 1是20加1等于21注意这里不是*(p1)它没有改变p自己的位置。第三次输出40此时p还在a[1]p 2指向a[3]解引用得到40。这道题我当年做的时候是顺的但我身边真有同学踩了坑。他第二问写的是21没错但解释的时候说“p先加了1所以输出201”。这就是典型的后置自增语义没吃透。后置自增的副作用在表达式求值之后才生效*(p)实际等价于先把当前位置取出来再移动指针。我复盘时觉得这种题错不是因为不会而是平时写业务代码时很少把指针自增、取值顺序揉在一起用。C语言开发者如果只在刷题时碰指针工作里全是vector和string这层敏感度就是建立不起来。这不光是应付笔试的问题写嵌入式代码、写网络底层解析、做高性能中间件的时候指针的每一步移动都要求你在脑子里能精确画出内存图。2.2 大小端与结构体对齐考的是你有没有真正写过字节流卷子中第二道C语言题没有给代码而是给了一个场景某设备通过串口发送一个四字节整数0x12345678小端模式在先收到的字节序列是什么紧接着又问如果定义了一个结构体typedef struct { char tag; int value; } Packet;在32位系统里sizeof(Packet)是多少第一问是大小端的基础小端模式低字节在前所以0x78、0x56、0x34、0x12这么传。第二问就是结构体对齐char占1字节后要填充3字节对齐到4字节边界然后int占4字节总共8字节。这道题难吗知识点本身不难。但它的组合方式很有意思——卷子把“发送字节序”和“内存布局”放在一起其实是在模拟真实的协议解析场景。你做网络通信、做驱动开发凡是需要把结构体直接映射到字节流的地方这两个问题都会同时出现。真正的坑在后面的追问“如果发送端是小端机器接收端是大端机器直接memcpy结构体会发生什么”答案很直接字段值会读反0x12345678在接收端会被解析成0x78563412。所以不能用“结构体指针强转”的方式做协议解析而是需要一个显式的字节序转换函数常见的做法是uint32_t ntoh32(const uint8_t *buf) { return ((uint32_t)buf[0] 24) | ((uint32_t)buf[1] 16) | ((uint32_t)buf[2] 8) | ((uint32_t)buf[3]); }这个函数就是“逐字节取然后移位组数”无论本机是大端还是小端只要预定义好协议是大端模式就能保证跨平台解析正确。我见过不少同学在项目里图省事直接Packet *pkt (Packet *)buf;然后访问字段开发机上一跑没问题交给测试或者在ARM板子上跑就莫名奇妙的乱码。这道笔试题目其实就是安全软件公司天天要面对的问题——你写的网络分析引擎、协议解析模块、流量检测探针哪一天不跟字节序打交道2.3 内存泄漏和悬垂指针为什么安全厂商这么爱问卷子里有一道多选题列出几个C代码片段问哪些存在内存管理问题。其中一个片段是char *getString() { char buf[64]; snprintf(buf, sizeof(buf), hello); return buf; }这是典型的返回栈内存地址函数返回后buf就失效了属于悬垂指针。还有些片段涉及new之后没有delete或者delete之后没有置空导致double free。这类题对干过嵌入式或者服务器开发的人来说是送分题但对只写过业务逻辑的同学来说容易忽视一个细节delete p之后如果不把p置为nullptr虽然不会立刻崩溃但一旦后续代码再次delete p就是未定义行为可能直接让free()检测到非法指针进程abort。我当时备考的时候总结过一个检查清单现在贴在工位上也一直有用栈内存绝不返回给上层调用者所有堆内存分配必须对应一个释放路径包括异常分支结构体内持有资源时需要显式定义拷贝构造、赋值运算和析构防止浅拷贝后double free尽量用std::unique_ptr / std::shared_ptr管理生命周期裸指针只在需要侵入式接口时使用为什么安全厂商对这块如此敏感因为很多安全漏洞的根源就是内存管理错误。缓冲区溢出、释放后使用、双重释放这些都是CVE里反复出现的漏洞类型。奇安信是做安全起家的笔试里嵌入这些题目不只是考语言更像是筛选“有没有安全编码意识”的候选人。3. 网络协议题不光要懂流程还要懂为什么出错3.1 TCP三次握手与四次挥手的时间线陷阱卷子里有一道关于TCP的问答给了个场景客户端主动断开连接问整个过程中双方状态是怎么迁移的。标准答案大家都会背FIN_WAIT_1、FIN_WAIT_2、TIME_WAIT、CLOSE_WAIT、LAST_ACK、CLOSED。但卷子没有停在状态名而是接着问“为什么主动关闭方要停留在TIME_WAIT状态等待时间怎么算”第一问等最后一个ACK可靠到达以及让旧连接上的延迟报文段在网络中消失。第二问等待时间是2MSLMaximum Segment Lifetime一般是1到2分钟。这个知识点本身不冷门但很多同学只背了个名次不知道实际在项目中意味着什么。我举一个真实场景你写一个高并发服务主动断开连接后如果把SO_REUSEADDR没设置好端口可能被TIME_WAIT占着服务重启时bind就会失败报Address already in use。这时候你必须了解TIME_WAIT存在的意义才能在“要不要开SO_REUSEADDR”这个问题上做权衡——开了可能复用端口但也可能接受到旧连接的迟到报文。这道题卷子里还配了一个追问如果服务端一直不调用close只调用shutdown(SHUT_WR)此时还能不能接收数据这个问题很刁钻因为shutdown(SHUT_WR)是关闭发送方向接收方向仍然开放对方ACK之后你可以继续收数据直到你也调用close或者收到对方的FIN。这在实际的协议设计中很有用比如HTTP的chunked传输或者一些半关闭状态的自定义协议。3.2 HTTPS握手为什么非对称加密不直接加密业务数据单选题里有一道“HTTPS建立连接的第一阶段客户端和服务端主要完成什么”选项有“协商对称加密密钥”“传输业务数据”“验证客户端身份”“建立TCP连接”。正确答案是验证服务器身份并协商对称密钥。其实整个过程分了几步证书校验、密钥协商ECDHE或RSA、然后用对称加密算法如AES-GCM加密业务数据。卷子又补了一道大题“为什么不直接用RSA加密所有数据”这个问题的本质是性能。RSA做1024位模幂运算比AES慢大概一到两个数量级而HTTP请求可能包含几十KB甚至几MB的body全部用非对称加密性能是不可接受的。所以HTTPS设计成混合加密用非对称/密钥交换算法打通一个安全通道然后协商一个对称密钥后续所有流量都走对称加密。这个“用非对称换密钥用对称加密单据”的模式几乎所有现代安全协议都是这么设计的比如SSH、TLS、IPSec。我在写这道题的解析时特别想强调一点很多同学记了“HTTPS HTTP SSL”但面试官一句话就能问穿——“那SSL和TLS到底差在哪”如果只知道前者是旧版本后者是新版本显然不够。简单说TLS 1.2之前有SSL 2.0/3.0、TLS 1.0/1.1这些老协议TLS 1.3又把握手步骤从两次RTT优化到一次RTT。做安全产品的人对协议版本的兼容性和禁用项都会非常敏感因为很多降级攻击就是利用服务器仍支持老版本协议。3.3 常见攻击题的答法从原理到防御要能一条龙说出来Web安全基础在安全厂商的笔试试卷里不会缺席卷子里出的是SQL注入和XSS。给了一段代码用户输入直接拼进SQL查询字符串问存在什么漏洞、如何修复。答案很简单SQL注入修复方式是参数化查询或预编译语句。但这个题如果只回答到“用预编译SQL”我觉得是不够的。我在复盘时认为真正能让阅卷人眼前一亮的答法分四步说清楚注入点字符串拼接导致用户输入变成了SQL代码的一部分说清楚危害拖库、绕过登录、删除数据、提权说清楚修复预编译语句让数据库把参数当数据处理而不是拼接成SQL说清楚纵深防御输入校验白名单、最小权限数据库账号、WAF规则兜底卷子里还考了一道CSRF问的是“转账请求中为什么需要token”。核心原因是CSRF攻击的本质是“浏览器自动携带凭证”。Cookie是自动携带的攻击者只要诱导用户访问恶意页面这个页面发出的请求就会自动带上用户的Cookie。Token放在请求体或自定义Header里第三方站点读不到也无法预先构造。这套逻辑我在后续工作里反复给别人讲防御CSRF的思路不是“验证请求来自谁”而是“验证请求是否由用户本人意图发出”。前者靠同源策略能挡一部分但挡不住跨站请求后者靠不可预测的token本质上是第二重身份认证。4. 操作系统与并发题场景题多直接背概念的题少4.1 进程线程区别的进阶考法常规送分题是“进程和线程的区别”但卷子换了个问法多线程模型下两个线程各自调用fork()子进程里会有几个线程这个问题考的是对Linuxfork()行为细节的了解。在Linux中fork()只会复制调用它的那个线程其他线程不会在子进程中继续运行。但是如果该线程正在持有一把锁而另一个线程在fork之前锁了资源没释放子进程里就可能出现死锁。所以很多服务器编程规范里强调多线程程序里尽量避免直接fork如果非要fork在fork之后尽快调用exec加载新程序或者在fork前做好锁的清理。这道题让我印象深是因为它把一个概念题进程线程区别和一个实践坑fork与锁结合在了一起。另一个相关题目是“什么场景下用多进程什么场景下用多线程”这题没有标准答案本质是考你权衡能力。我当时答的是需要高稳定性、隔离性强的模块用多进程一个进程崩溃不影响其他进程需要高并发IO、共享大量内存状态的模块用多线程因为线程上下文切换成本低、共享数据方便。但必须补一句“多线程共享内存也让bug排查变得很困难”。安全软件里检测引擎和内核态组件往往承担着“不能崩”的职责我见过不少企业级安全产品把数据采集、策略执行拆成多个独立进程就是为了防止单点故障扩大化。4.2 生产者-消费者模型的三个考点互斥、同步、条件变量卷子里有一道手写/补全题给了生产者-消费者模型的骨架要求用互斥锁和条件变量补全逻辑。核心代码大致长这样pthread_mutex_t mutex PTHREAD_MUTEX_INITIALIZER; pthread_cond_t cond PTHREAD_COND_INITIALIZER; int buffer -1; void producer() { pthread_mutex_lock(mutex); buffer produce(); pthread_cond_signal(cond); pthread_mutex_unlock(mutex); } void consumer() { pthread_mutex_lock(mutex); while (buffer -1) { pthread_cond_wait(cond, mutex); } consume(buffer); buffer -1; pthread_mutex_unlock(mutex); }几个关键点我必须强调。第一pthread_cond_wait必须在while循环里不能用if。因为条件变量可能被异常唤醒spurious wakeup即使没有signalwait也可能返回所以必须重新检查条件。第二等待前必须先加锁因为条件变量的语义就是“等待时释放锁被唤醒时重新获得锁”。第三pthread_cond_signal本身不释放锁你要在unlock之后再signal或者signal之后立刻unlock否则消费者被唤醒后抢不到锁性能会受影响。卷子追问了一句“如果只有一个生产者和一个消费者用互斥锁保护buffer是否足够”答案是足够了但这里会有一个隐性效率问题如果消费者轮询bufferCPU占用会很高用条件变量则是事件驱动只有在有数据时才唤醒这是需要条件变量的真正意义。4.3 死锁题四条件之外面试官还想听到预防策略死锁也是必考点但卷子出了一道很接地气的场景题两个线程分别持有锁A和锁B然后都想获取对方的锁会发生什么这就是典型的循环等待死锁。四个必要条件互斥、持有并等待、不可剥夺、循环等待。问预防策略时我答了四个方向破坏互斥很难做到因为锁本身性质就是互斥破坏持有并等待一次申请所有资源破坏不可剥夺超时释放比如pthread_mutex_trylock定时重试破坏循环等待锁顺序一致比如所有线程都先锁A再锁B实际工程里锁顺序一致是最常用的手段也是代码评审中重点检查的问题。我后来在工作里经历过一次死锁故障排查半天才发现是模块A先锁X再锁Y模块B先锁Y再锁X两个模块联动时触发了死锁。修正方式就是规定全局锁的获取顺序违反的代码直接标记为bug。5. 数据结构与算法手撕题稳定和边界是考察重点5.1 链表反转一个bug free率最低的题卷子里有一道大热题——单链表反转。看起来简单但要求是不用递归、手写完整代码并且处理链表为空或只有一个节点的情况。我建议的标准迭代写法是struct ListNode* reverseList(struct ListNode* head) { struct ListNode *prev NULL, *curr head; while (curr) { struct ListNode *next curr-next; curr-next prev; prev curr; curr next; } return prev; }这道题的常见错误忘记保存next指针结果还没断链就先丢了后面的节点循环条件写成curr-next ! NULL导致最后一个节点没反转返回值写成curr此时curr已经变成NULL正确应该返回prev我面试别人的时候经常用这个题因为它特别适合考察“会不会写边界条件”和“有没有在纸上提前走一遍过程”。笔试时没有编译器的帮助写不写得出严谨的代码很见真功夫。5.2 Top K问题的“高频词”变体卷子里的高频题是给定一个文本文件统计出现频率最高的K个单词。这题有两部分用哈希表统计频次然后用最小堆维护当前频率最高的K个堆大小为K堆顶是K个中最小频次的单词新词频次大于堆顶就替换并堆化。最后堆里剩下的就是Top K。时间复杂度是O(n log K)n是所有单词数量。空间复杂度是O(n)的哈希表加O(K)的堆。这道题还有一个隐含考点如果文件太大内存放不下怎么办。答案用外部排序/分治分片统计后归并或者用Trie树在流式处理中直接维护频次。我实际处理过几十GB日志的频次统计单机内存扛不住当时就是用“分片Hash 小顶堆归并”的方案跑的思路和笔试完全一致。5.3 动态规划题不要一上来就写代码先定义状态最后一道算法题是经典爬楼梯问题的变体每次可以爬1阶或2阶问爬到n阶有多少种方法。这个最基础方法是dp[i] dp[i-1] dp[i-2]但卷子狡猾在把“1阶或2阶”改成了“每次爬的阶数是质数”。这就有意思了因为题目变成每次可以爬2、3、5、7……之类的质数阶。递推思路没变dp[i] sum(dp[i - prime]) for prime in primes and prime i边界条件dp[0] 1表示在起点的一种方案。我用一个例子验证n4时可以22但不能131不是质数也不能31所以答案是1。如果你按“求所有走法”的误区做就会多算很多路径。这题给我最大的体会有两点一是看到动态规划别急先把状态定义和转移方程写清楚再写代码二是很多变体题只是换了外层约束底层还是同一套套路。笔试时时间紧但状态定义这一步绝对不能省一旦方向错了代码写得再快也是白费。6. 安全开发流程与综合题这是安全厂商的特色菜6.1 从“安全左移”角度看软件开发流程卷子的综合题部分有一道简答“你认为在软件开发过程中安全应该从哪个阶段介入请简要说明理由。”这道题本质是考察有没有“安全左移”Shift Left的意识和SDLC软件安全开发生命周期的概念。常规答法是需求阶段进行威胁建模识别潜在的攻击面设计阶段评审架构安全性和数据流开发阶段代码规范检查、静态代码扫描、代码评审测试阶段动态扫描、渗透测试、模糊测试部署运维阶段安全配置基线、漏洞监控、应急响应答这道题时我不建议写成教科书的罗列而是用一个例子串起来比如设计一个登录模块需求阶段就要考虑撞库和暴力破解设计阶段就要决定是用JWT还是SessionToken放Cookie还是LocalStorage开发阶段要确保代码里不把密码明文打日志测试阶段用自动化工具尝试SQL注入和弱口令上线后还要关注第三方组件漏洞库。这样既显得有条理又能证明你真的经历过。6.2 一个开放性设计题如何防刷卷子最后有一道开放性题给一个短链接服务要求防滥用、防刷怎么设计。这类题目没有标准答案但可以按下面的框架拆解限制维度IP、用户账号、设备指纹、Session限流算法令牌桶/漏桶/固定窗口选一种并解释为什么频率限制比如单个IP每分钟最多生成20个短链接验证码超过阈值后弹验证码业务层对高频生成做人工审核或动态延长审核周期监控生成量突变时告警方便安全运营介入这种题我最怕听到的回答是“上验证码就完了”。验证码只是最后一道闸真实场景中攻击者可以换IP池绕过。所以在设计阶段就考虑多层组合并把“监控和告警”纳入方案是安全公司比较看重的思维习惯。6.3 结合热词说一点嵌入式与移动开发方向怎么借鉴这套题我注意到热词里出现了“嵌入式软件开发”“移动软件开发”“ASPICE软件开发流程”这说明现在准备软件开发校招的候选者范围很广不只是纯互联网方向。这套奇安信试卷对嵌入式方向的同学来说参考价值更高因为试卷里大量内容本身就是嵌入式开发的基本功内存布局、字节序、指针操作、资源管理。嵌入式设备做协议栈、做固件升级、做驱动全都绕不开这些。对移动开发方向的同学这套卷子的安全基础题更有启发因为移动端同样面临数据安全、通信安全、组件安全等问题。虽然题型不像Android/iOS专项笔试那样考四大组件或RunLoop但“安全编码意识”是通用的。做移动端也经常要处理HTTPS证书校验、数据加密存储、防重放攻击这些底层原理和这套卷子考的东西是相通的。我建议不同方向的同学不要只刷本方向的题抽空做一份其他方向的笔试试卷往往能发现自己的知识盲区。我当年就是刷了一套嵌入式笔试题之后才对字节序和内存对齐有了真正的肌肉记忆后来做流量分析项目时直接受益。7. 答题策略与复盘经验这些坑现在说出来就是为了让你别踩最后聊一点实用经验。这套卷子我做完之后复盘发现失分的点往往不是“不会”而是“毛躁”。时间分配上我的建议是选择题和填空题控制在25到30分钟内因为这类题每题分数不高不能纠结太久程序设计题和简答题优先做因为这类题分值高且需要思考留足时间开放设计题安排在最后想好框架就动笔不要追求完美。读题上遇到“正确的是”“不正确的是”“可能导致问题的是”这类反向设问先在题干上划线标出来防止选反。我在卷子上确实犯过这个错误不是因为不懂是因为读得太快。代码题上优先保证思路清晰再考虑优化。很多同学一上来就写O(n)的优化解法结果边界条件一堆bug。我的建议是如果时间紧张先写一个正确但可能不是最优的解法比如先写O(n^2)的暴力解然后再在注释里提一句“可以优化到O(n log n)”。阅卷时正确性比最优性重要得多。最后分享一个我复盘时的心得不要只背“知识点”要把知识点还原到场景里。比如你背了“大小端”就想想自己写序列化模块时怎么处理你背了“TIME_WAIT”就想想为什么服务重启会报端口占用。这套卷子的特别之处就在于它逼着你做这种还原。这种从概念到场景的思维转换不是考前几天能突击出来的而是靠日常写代码时多问几个“为什么”。做完这套卷子之后我养成了一个习惯每学一个底层知识就主动在自己的代码里找找有没有对应场景找不到就编一个小demo验证一下。这个方法对准备任何技术笔试都适用你可以试试。
返回列表