奇安信终端开发笔试复盘:C/C++与系统底层考点全解析

发布时间:2026/8/31 22:17:36
奇安信终端开发笔试复盘:C/C++与系统底层考点全解析 2019年春招我面的是奇安信终端开发岗。笔试那场虽然过去几年了但到现在印象还是很深。整张卷子没有太多花哨的东西C/C占了将近一半剩下的大头是操作系统原理、进程线程、以及一堆从安全软件实际场景里抽出来的问题。当时考完觉得自己答得还行等拿到结果再看才发现有些分丢在很固执的地方上。这篇文想把那次笔试的题型、思路、以及我个人复盘出来的坑写透给后来准备类似岗位的同学一个参考。终端开发这个方向在安全公司里其实是一个非常典型的工程师含量很高的岗位。不同于后端写业务接口终端开发要直接和操作系统打交道要考虑稳定、兼容、防对抗、性能损耗要打交道的是几十万上百万台机器上形态各异的环境。奇安信做的是终端安全的底座无论是网络准入、病毒查杀、主机审计还是零信任里的终端感知模块最底层跑的都是一套要长期驻留在用户机器上的客户端。写这套东西的人笔试里如果不考指针、不考系统原理、不考堆栈布局那才叫奇怪。下面把那次笔试拆开来看。我会按题型划分把典型的题目逻辑、正确的打开方式、以及我当时做得不够好的地方一起说清楚。1. 终端开发笔试岗位需求与出题逻辑1.1 从产品反推技术栈先说一个很多人容易忽略的点笔试不是考你背了多少书而是在筛能不能处理这个团队真实问题的人。以奇安信终端安全产品线为例。这类软件最终要跑在用户机器上要占资源少、不崩溃、不拖慢开机速度还要能在恶意代码已经运行的情况下把对方清理掉。这就决定了终端开发工程师日常接触的几乎都是底层能力系统核心 API 的调用与拦截文件系统与注册表的监控进程启动和线程注入的检测网络数据包的解析与过滤内核回调、驱动与用户态程序之间的通信安装包、升级、兼容性和稳定性相关的处理所以笔试题里出现大量 C/C 和 Windows/Linux 系统编程的内容不是出题人偷懒而是岗位真正需要。我记得2019年春招的卷子里第一板块基本都是语言基础而且是那种“看着简单但陷阱密集”的问题。这和很多互联网公司喜欢考算法题的路子不一样更偏工程向、底层向。1.2 考点背后的能力层次我把那场笔试的题目分成了几个层次这样看会更清楚第一层工具人层。会不会写 C/C指针和内存的管理是否顺手。这部分题如果答不好基本就和终端开发无缘了。因为终端开发中任何一个内存错误都可能让整个客户端崩溃甚至拖垮用户的系统。第二层工程师层。懂不懂进程和线程的本质能不能处理并发和同步能不能理解动态库是怎么被加载的。终端软件里大量使用多线程来处理实时监控、日志上传、扫描任务这个问题避不开。第三层安全工程师层。能不能用攻击者的角度去看代码。笔试里那些看起来像找漏洞的题目其实是在在考察你有没有安全直觉。写终端安全软件的如果自己写代码都没安全意识那产品上线就变成筛子。我当时对这第三层的体会还不够深直到后来自己真的开始做主机安全相关的东西才明白试卷里那些路径遍历、格式化字符串、整数溢出题目一点都不超纲。2. 真题盘点四类必考题型与解题思路这场笔试的题目没有对外公布过我这里只能根据个人回忆和同期一起笔试的同学复盘整理成四类高频题型。每一类我都会把核心考点、典型问法和标准解题思路讲清楚。2.1 C/C 语言基础指针、内存与对象生命周期这一板块是绝对的送分题也是绝对的送命题。说送分是因为只要基础扎实几乎不需要思考。说送命是因为太多人栽在以为自己会的地方。我记得出现频率最高的几类题第一类指针与内存。比如问你char *p hello; p[0] H;会不会崩溃答案会。因为字符串字面量通常存在只读区修改它属于未定义行为。还有int a[5]; int *p a; p3指向哪里这类问题考察数组与指针的等价性。第二类手写 strcpy 或 memcpy。这个看起来简单但完整得分不容易。要写出能防止内存重叠的版本需要判断dest和src的地址关系。如果src在dest前面且两者有重叠从前往后复制会覆盖后半段数据必须改成从后往前复制。我当时写的版本大概是这样void *memmove(void *dest, const void *src, size_t n) { char *d (char*)dest; const char *s (const char*)src; if (d s || n 0) return dest; if (d s) { for (size_t i 0; i n; i) d[i] s[i]; } else { for (size_t i n; i 0; i--) d[i-1] s[i-1]; } return dest; }注意这里和memcpy的区别。很多标准库的memcpy不保证处理重叠区域如果你题目里要求的是memcpy得先确认是不是要求安全版本。我当时就遇到了追问如果src和dest有重叠你的程序会出什么问题这就是在引导你补充边界处理。第三类构造函数、析构函数、拷贝构造、赋值运算符。这里喜欢考继承体系下的构造析构顺序。比如一个派生类对象创建时先调用基类构造函数再调用成员对象构造函数最后才执行派生类构造函数体。析构顺序完全相反。当时有一道题让我印象很深class A { public: A() { std::cout A; } virtual ~A() { std::cout ~A; } }; class B : public A { public: B() { std::cout B; } ~B() { std::cout ~B; } }; int main() { A *p new B(); delete p; }输出是AB~B~A。考的就是虚析构。如果没有virtual ~A()delete 基类指针时只会调用基类析构派生类的资源就泄漏了。终端软件里大量使用多态比如扫描引擎抽象基类、不同文件类型的解析器这个问题答错基本就是凉。实用建议准备这类题不用刷太多重点是把《Effective C》前几个章节吃透特别是构造、析构、赋值、资源管理这几块。手写代码时先想清楚函数签名再写实现最后在心理跑一遍测试用例很多低级错误都能避免。2.2 数据结构与算法链表、树与生产者消费者奇安信这类笔试里算法题难度不算顶但很实用。不会考复杂的红黑树手写也不会考动态规划难题更多是写链表、二叉树遍历、线程同步场景题。链表反转是我面试经历中出现率最高的算法题笔试也考了。这个题既要会迭代法也要会递归法。我建议两种都要写一遍因为面试官可能会追问递归版本的空间复杂度是多少答案 O(n)因为递归调用栈会占用额外空间。迭代版 O(1)。二叉树层序遍历也考了。实现 BFS 时要用队列而且要能分清每一层的边界。笔试时我见过一个变形题之字形打印二叉树。这个比普通层序多了一个翻转层的操作判断奇偶层后用reverse或者双端队列都可以。生产者消费者是我认为所有终端开发笔试里最值得重视的题因为它直接对应真实业务一个线程不停地从系统回调队列里取事件另一个线程负责处理事件并写入日志。笔试可能会让你用 C11 的 mutex 和 condition_variable 写出一个无 BUG 的版本或者让你指出下面代码的问题。一个典型的问题代码是std::queueint q; std::mutex mtx; std::condition_variable cv; void producer() { while (true) { std::unique_lockstd::mutex lock(mtx); q.push(1); cv.notify_one(); } } void consumer() { while (true) { std::unique_lockstd::mutex lock(mtx); if (q.empty()) { cv.wait(lock); } int v q.front(); q.pop(); } }这段代码的问题在于cv.wait可能被伪唤醒也就是说不是真的有人 notify线程也可能醒来。如果醒来时队列还是空的直接取数据就出错。正确写法是把 if 改成 while或者用带谓词的cv.wait(lock, []{return !q.empty();})。另外还要注意 notify 时持锁与否通常建议 notify 放在锁外但这并不是硬性要求。我当时写的时候其实也没意识到伪唤醒这个细节是后来做抓包分析里的实时日志时才真正重现了这种问题。笔试时如果还没准备好就记住一句话条件变量等待要用循环去判断条件。2.3 系统原理进程、线程与动态库机制这一板块在整张卷子中占比非常高也是最能拉开差距的地方。终端开发必须在用户态甚至内核态理解操作系统的行为。进程与线程的本质区别。这道题几乎必考但很多人只答出进程有独立地址空间线程共享地址空间。如果你在安全公司笔试里这样回答得分很低。应该补充线程是操作系统调度的基本单位拥有独立的栈和寄存器上下文共享进程的堆、全局变量、文件描述符进程是资源分配的基本单位上下文切换代价高于线程线程崩了可能导致整个进程崩溃进程之间互相隔离。再进一步可以提到clone系统调用在 Linux 中是通过 flag 控制共享和隔离粒度的。Windows API 相关的题。终端软件开发大部分时间都在和 Win32 打交道。我记得笔试里有道题是写出获取当前进程 ID、创建线程、等待线程结束三个 API 名称。分别是GetCurrentProcessId()、CreateThread()、WaitForSingleObject()。这道题本身不难难的是后面跟着一题如何安全地结束一个线程。很多人写TerminateThread()但在安全开发里这是极端危险的做法。因为系统不会执行该线程的析构函数、不会释放栈、不会恢复锁状态可能导致整个进程死锁或内存泄漏。正确做法是设置一个事件或标志让线程函数自己退出。动态库导出和 LoadLibrary。这道题很经典你在一个 DLL 里写了一个导出函数名字叫foo但用LoadLibrary后调用GetProcAddress(handle, foo)返回空。为什么一个很常见的原因是名字修饰。C 编译后函数名会被修饰成包含参数类型的形式如果导出定义里没有加extern C实际符号名就不是foo。处理办法是用.def文件或extern C声明。这个知识点在做插件、Hook、注入相关功能时天天遇到。Linux 下的共享内存和信号量也是常客。终端安全产品在 Linux 服务器上经常要采集审计日志、文件变化多个进程之间需要通信。有一道题是写出 System V 共享内存的常用函数shmget、shmat、shmdt、shmctl。如果只答出来了shmget后面几个答不出来说明实际动手经验还欠缺。2.4 安全编码从路径遍历到缓冲区边界笔试里最有奇安信味道的题目是安全编码相关的问题。终端开发尤其是安全软件客户端代码质量直接决定了产品能不能防住恶意攻击。路径遍历是我记忆里出现过的明确考点。题目可能给你一段代码要求找出任意文件读取漏洞。比如根据用户输入的name参数拼接路径后直接fopen攻击者传一个../../../../etc/passwd就能读到任意文件。修复方式是要做规范化用realpath()或std::filesystem::canonical()解析完整路径并检查结果是否在允许的根目录内。这个场景在终端软件的配置加载、文件扫描、安装包释放中都可能存在。我当时虽然知道路径遍历但没有立刻反应过来还以为是简单的 文件不存在 处理问题。这就是安全直觉没到位。缓冲区溢出。笔试考过一道代码审计题让你找出char buf[64]; gets(buf);这一行代码的问题并说明攻击者如何利用。答案很简单gets不检查边界攻击者可以构造超长字符串覆盖栈上的返回地址劫持控制流。修复方式是使用fgets(buf, sizeof(buf), stdin)。这类题对熟悉安全的人来说是白给但对没接触过的人可能只是想数组越界了描述不到利用层面。在终端开发的笔试里能提到底层利用思路是加分项说明你真懂这类攻击。整数溢出。有个经典场景int size GetInputSize(); char *buf new char[size]; memcpy(buf, data, size 1);当size为-1时size 1溢出为 0导致memcpy不复制任何数据当size为最大值时size 1变成INT_MIN传给 memcpy 时被转换为巨大无符号数直接崩溃或栈溢出。处理原则是任何外部输入参与长度计算时必须在运算前做范围检查。格式化字符串漏洞。题目往往很简单printf(user_input);哪里错了答案是用户输入中包含%s、%x这类格式占位符时printf会从栈上取参数泄露内存内容严重时还能写内存。修复方式是printf(%s, user_input);。这些安全编码题看起来是很基础的攻防知识但在真实笔试中往往和内存、指针、系统 API 揉在一起考察。它不是考你会不会用工具而是考你写代码时能不能形成肌肉记忆。3. 答题过程中的坑与二次复盘心得3.1 最容易丢分的不是不会是想当然我第一次做这套题的时候最大的问题就是想当然。比如strcpy和memcpy的区别我写的时候脑子里很清楚strcpy会复制到\0为止memcpy按字节数复制。但笔试题里给了一行代码char buf[16]; strcpy(buf, hello world, hello qax);问buf中内容是什么。我下意识觉得是 hello world因为数组大小是 16字符串太长会被截断。但这个题目其实想考的是缓冲区溢出strcpy根本不检查目标缓冲大小它会一直复制到字符串结束覆盖buf之后的内存结果是未定义行为。不是简单截断。这就是想当然坑。很多看起来是基础题的问题实际都是引你往错误的理解上走。后来我复盘的时候总结了一条经验终端开发笔试里的 C/C 题只要看到strcpy、sprintf、gets这类函数第一反应应该是不安全而不是它会正常截断。3.2 手写代码的规范性与可读性是隐性加分项笔试的时候手写代码可能不会被实际编译运行但阅卷人会看。工整的缩进、清晰的命名、边界条件的处理这些都很重要。我见过很多同学代码逻辑是对的但变量名全是a、b、c也不写注释。在工程导向的终端开发岗位面试里这是大忌。因为你进入团队后写的代码要给别人 review要保持很长时间风格不好甚至会返工。我写一个链表反转的时候除了实现本身还会在草稿上标注清楚每个指针的含义// p : 当前节点 // prev : 已反转部分的头节点 // next : 保存原链表的下一个节点防止断链这样即便某个细节写错了阅卷人也能看出你是真理解而不是背代码。3.3 时间分配不要在小题上死磕整套卷子的题型分布很广有的同学看到前面几道 C 选择题有点不确定就反复琢磨结果后面的大题反而没时间写。我当时的策略是先把所有会做的题目快速过一遍不会的做标记全部完成后再回头处理。为什么这样更合理因为笔试的评分往往有按点给分的特点。你一道大题写了核心思路、关键代码、边界检查能拿到不少分。而一道选择题想再久也可能只有对或错两种结果性价比很低。终端开发笔试题量不轻时间管理很重要。4. 从笔试看终端开发的实际工作模型4.1 一套终端安全软件的日常链路如果只说笔试不说这些知识和真实工作有什么联系那这篇复盘就不完整。终端开发日常在做的事情是这样的客户端启动后以系统服务或驻留进程的形式挂在后台。它要不断接收操作系统的事件通知比如进程创建、文件写入、注册表修改。收到事件后解析关键参数交给规则引擎和行为引擎做判断。如果判定为恶意就阻止动作、隔离文件、弹窗提示用户。这个链路里每一个环节都能对应上笔试的题目进程创建事件的捕获需要懂 Windows 的PsSetCreateProcessNotifyRoutine或者 Windows 的事件追踪机制可以扩展到用户态 Hook。文件写入事件的判断需要知道文件句柄、路径规范化、是否涉及符号链接或路径穿越。多事件同时到达时需要生产者消费者队列来缓冲和处理避免阻塞系统回调。规则引擎跑在独立线程池里线程同步、锁粒度、死锁规避都是基本能力。客户端升级时要能处理进程文件被占用、动态库版本不兼容、系统回滚等问题。所以那些看起来基础的笔试题其实已经提前划定了这个岗位的工作方式。笔试不是在为难你而是在预演你的日常工作。4.2 回头看怎样的答案能拿高分我后来也帮部门看过一些新人笔试的评分发现高分答案往往有这几个特征特征一写代码前先明确边界条件。比如写一个判断字符串是否回文的题多数人直接循环。高分答案会先考虑空字符串、单字符、大小写、标点符号、奇偶长度。这类边界思维在终端软件的输入处理中至关重要因为客户端要面对的现实场景永远比你想到的多。特征二能讲清楚为什么。有的题目不要求你写代码而是给你一段代码问你有什么问题。低分答案是直接说这里会崩溃。高分答案是先说问题在哪一行为什么会导致崩溃攻击者可能怎样利用修复建议是什么甚至还会补充类似的代码模式还有哪些风险点。特征三安全立场要正。这行是写给所有想进安全公司做开发的人看的。安全公司的产品是给人做防护的笔试中考察的你如何修复漏洞、如何处理恶意样本、如何设计防御策略都是为了让你成为建设者而不是破坏者。答题时不要表现出对攻击技术的盲目兴奋而是要体现我懂攻击为了更好地防御的心态这是评卷人非常看重的价值观。4.3 过了笔试之后还要做什么笔试只是第一关。奇安信终端开发的面试流程中技术面会继续深挖你在笔试中暴露出的薄弱点。我那次笔试中有一道 Windows 进程注入相关的问题只答了一半面试时就被追问了整体思路。所以建议所有笔试结束的同学不要对答案对完就完了。正确做法是把每道错题还原成一个知识主题然后系统过一遍。我当时给自己列的查漏清单大概是这样的C/C构造析构顺序、虚函数表、内存对齐、智能指针、右值引用数据结构链表、栈、队列、二叉树、哈希表操作系统进程线程模型、锁机制、条件变量、内存映射、静态库和动态库Windows 编程窗口消息机制、DLL 注入常见方法、PE 格式、注册表、服务程序安全编码缓冲区溢出、整数溢出、格式化字符串、路径穿越、SQL 注入针对有数据库交互的后端模块网络基础TCP/IP、HTTP、DNS以及终端设备上报数据时用到的加密通信方式如果你能把这份清单里的内容用能给别人讲明白的标准过一遍笔试和面试都有很大胜算。5. 我的备考经验与最后的小建议2019 年春招那段时间我自己的状态其实是比较焦虑的。做了一套又一套互联网公司的笔试题算法题刷了不少但到了奇安信这套卷子发现自己最大的短板并不是算法而是系统底层 工程细节。后来我调整了策略不在刷题数量上较劲而是把一份典型笔试题拆开研究。每个考点都自己在电脑上写小实验跑一遍比如用 C 写一个带虚析构的继承链用WaitForSingleObject写一个多线程累加用LoadLibrary加载一个自己写的 DLL 并验证导出函数名。这个过程比刷 100 道 LeetCode 对终端开发岗更有用。最后再分享一个小技巧也是我后来做终端开发时悟出来的笔试中如果遇到完全没思路的题不要空着。写一个我目前的理解 最可能的解决方向也比空题好。一方面阅卷老师能看到你的思维过程另一方面这和你以后在团队里遇到陌生问题时的处理方式一致。终端开发的世界里几乎不会有完全按文档跑通的情况遇到未知是常态关键在于你有没有一套分析方法。希望这篇复盘能帮准备终端开发岗位的同学少走一点弯路。笔试没有太多捷径但对的方向比盲目努力重要得多。把这些基础吃透、把手写代码练熟、把安全编码的直觉养出来机会自然会来。