这套卷子我反复看了好几遍,2020年奇安信秋招软件开发方向试卷3,整体风格和互联网大厂的开发笔试很不一样。普通互联网公司考的是算法题刷得勤不勤,奇安信这套卷子明显在暗示一件事:我们招的是写安全产品的开发,不是只会写CRUD的码农。卷子里藏着不少安全编码意识、内存布局、输入校验这类考点,如果你只是刷LeetCode冲过去,选择题可能会先栽一轮。
我认识几个当年一起秋招的朋友,考完交流了一下,大家的共识是:这套试卷的编程题难度不算顶格,中等偏上,但选择题和填空题覆盖面很广,C++、操作系统、网络、数据结构、安全基础全都有。所以这篇文章我就把整套卷子的考察方向、核心考点、编程题思路,以及安全公司开发岗笔试特有的“隐藏分”一次讲清楚,给后面准备安全方向开发岗的同学做参考。
1. 试卷整体结构与考察方向拆解
1.1 题型分布和分值节奏
先说整体结构。按照2020年那场秋招的常见安排,软件开发方向试卷一般是90分钟到120分钟,题型大概分为四块:单选、多选、填空、编程。分值上编程题通常占大头,单题20到30分,选择和填空单题分值不高但数量多,加起来也能决定你能不能进下一轮。
我当时估算的分布大致是:
| 题型 | 题量 | 单题分值 | 考察重点 |
|---|---|---|---|
| 单选题 | 20道左右 | 2分 | 数据结构、C++/Java基础、操作系统、网络 |
| 多选题 | 5道左右 | 3分 | 概念辨析、边界条件、安全编码 |
| 填空题 | 5道左右 | 2分 | 输出结果、算法复杂度、简单计算 |
| 编程题 | 2道 | 20-30分 | 链表/动态规划/字符串处理 |
这套分布不算激进,但有一个很明显的倾向:基础题占了一半以上的分数。也就是说,哪怕编程题只AC了一道,只要选择和填空正确率高,整体成绩不会太难看。反过来,如果觉得编程题难,直接把前面基础题做扎实,也能和其他人拉开差距。
多选题是典型的丢分重灾区。普通互联网公司笔试多选少,而且多选通常就考一两道概念题。奇安信这套卷子里的多选偏爱考“哪些说法正确”“哪些操作可能导致内存泄漏”“哪些HTTP方法不安全”这类组合判断,少选多选都不得分。这就逼着你必须把概念掌握到“能判断对错”的颗粒度,而不是“大概知道”。
1.2 从考点倒推岗位画像
把整套卷子的考点串起来看,基本能还原出这个岗位要什么样的人。首先,编程底层要扎实,C++/Java内存模型得清楚,因为安全产品很多要跟底层系统打交道,缓冲区溢出、野指针这类问题在普通业务代码里是bug,在安全产品代码里就是漏洞。其次,网络基础和操作系统不能虚,网络安全产品大量涉及报文分析、进程监控、权限控制,不理解TCP状态转换、进程调度、用户态内核态切换,很多业务场景根本推不动。最后,具备基本的安全编码意识,写代码时知道用户输入是不可信的、路径要做规范化、格式化字符串不能直接拼用户数据,这些都是安全公司开发区别于普通开发的核心能力。
所以这套卷子本质上在筛选“底子好、有安全敏感度”的工程师,而不是单纯筛算法竞赛选手。想投安全方向开发岗的同学,备考思路就必须跟着这个画像走。
2. 基础考点复盘:容易被刷下去的选择题和填空题
2.1 数据结构:概念题比手写题更阴
数据结构这块,编程题可能会考链表和动态规划,但选择题考得更细,全是概念辨析。我印象比较深的有这么几类。
第一类是二叉树的性质。给定一棵完全二叉树有n个节点,问叶子节点有几个、深度是多少、第i个节点的父节点下标是多少。这类题要求你对完全二叉树数组存储的结论很熟:父节点下标是i/2、左孩子是2i、右孩子是2i+1,以及叶子节点数为n/2向上取整。如果这些结论长期不碰,临时推导虽然也能推出来,但会浪费时间。
第二类是排序算法的稳定性和复杂度。选择题会直接问“下列哪些排序算法是稳定的”,答案里会混着快排、堆排、归并、插入排序。稳定排序有冒泡、插入、归并、基数,不稳定排序有选择、快排、堆排。这个结论本身不难背,但题目不会只考稳定,它还会叠加“最好/最坏时间复杂度”来出多选,比如“下列说法正确的是:快排最坏O(n^2)、堆排空间O(1)、归并稳定且平均O(n log n)”。这种组合判断最容易在多选里翻车。
第三类是哈希与B+树。哈希冲突处理方式要能说出开放定址法和链地址法的区别,装载因子对性能的影响。B+树则爱考“为什么数据库索引用B+树而不是B树”——非叶子节点不存数据可以放更多索引项、叶子节点用链表串起来方便范围查询,这两个理由是高频答案,但单选题很少直接考原话,它会给四个选项让你判断哪句描述错了,比如把“叶子节点存储所有数据”说成“非叶子节点也存储全部数据”来误导你。
我的建议是,数据结构备考不要只刷算法题,把课本里那些“结论性内容”过一遍,尤其是复杂度、稳定性、存储结构特征。安全公司笔试对这些基础概念的考查密度远高于普通互联网公司。
2.2 网络与操作系统:面试前必背的几类结论
网络题在卷子里占的比例不小,而且安全公司考网络有一个特点:偏爱考“状态”和“协议交互细节”,不是简单问TCP和UDP有什么区别。比如TCP三次握手和四次挥手时各个状态怎么迁移,SYN_SENT、ESTABLISHED、FIN_WAIT_1、TIME_WAIT这些状态在什么条件下出现。2020年那套卷子里就有一道选择题问“主动关闭方在发送最后一次ACK后进入什么状态”,答案是TIME_WAIT,但选项里混了CLOSE_WAIT和LAST_ACK,如果不清楚四次挥手的过程,很容易选成CLOSE_WAIT。
HTTP相关考点也很安全公司特色,比如HTTP状态码语义、GET和POST的区别,以及“哪些HTTP方法是幂等的”。普通开发笔试可能只问GET和POST参数位置,安全公司会追问幂等性,因为这在设计RESTful API和防护策略时都是基础概念。
操作系统部分主要考进程和线程、调度算法、死锁条件。进程间通信方式(管道、消息队列、共享内存、信号量)是高频考点,尤其要清楚共享内存为什么效率最高,但需要同步机制配合。死锁的四个必要条件(互斥、请求保持、不可剥夺、循环等待)也常考,有时候会换一种问法:“破坏循环等待条件可以预防死锁,对应哪种策略”,答案是资源有序分配。这些概念都不难,但知识点杂,建议考前集中背一遍,尤其是那些“4个条件”“3种状态”“5种通信方式”之类的结构化结论,选择题里非常吃香。
2.3 编程语言与内存布局:C++/Java考点对照
卷子里编程语言相关的题目,明显偏向C++和底层内存,这是安全公司开发岗的特点。如果是Java方向岗位,也会考JVM内存区域和垃圾回收,但整体占比不如C++高。
C++考点里,虚函数和虚表是绝对的重点。题目会问含有虚函数的类实例化后内存布局中第一个成员是什么,答案是虚表指针;或者问虚析构函数的作用,选项里会出现“防止内存泄漏”和“正确释放派生类资源”,后者才是本质原因。RAII、智能指针(unique_ptr、shared_ptr)也是高频点,多选会问“哪些做法可以有效避免内存泄漏”,选项包括使用智能指针、new和delete成对出现、容器存放裸指针后手动遍历释放等。
内存分区题也很常见:局部变量在栈上、new出来的对象在堆上、全局变量和静态变量在全局区、常量字符串在只读常量区。安全公司考这个不只是考概念,它背后是缓冲区溢出和栈布局的底层逻辑,所以这类题分值不高,但在安全方向的面试里属于“你以为考的是C++,其实考的是漏洞利用基础”的思路。
Java方向如果出现,常考的是final关键字、String与StringBuilder区别、HashMap底层结构、并发包里的ConcurrentHashMap分段锁/CAS机制。虽然Java方向不会深挖缓冲区溢出,但并发编程的考点同样能筛掉一批人。
3. 编程题核心:高频题型的思路与模板
3.1 链表反转:递归与迭代两条路都要会
链表题在安全公司笔试里出现频率非常高,2020年奇安信这套卷子编程题里就有一道链表相关的题。链表题之所以受欢迎,是因为它既能考指针操作基本功,又能在O(1)空间限制下考逻辑能力,而且边界条件多,很容易区分水平。
最经典的就是单链表反转。我建议至少掌握两种写法,不仅为了这道题本身,更因为很多复杂链表题都是它的变体。
迭代法思路清晰,用三个指针prev、cur、next遍历,每次把cur指向prev,然后整体后移。代码模板:
struct ListNode { int val; ListNode* next; ListNode(int x) : val(x), next(nullptr) {} }; ListNode* reverseList(ListNode* head) { ListNode* prev = nullptr; ListNode* cur = head; while (cur != nullptr) { ListNode* next = cur->next; cur->next = prev; prev = cur; cur = next; } return prev; }递归法代码更短,但理解门槛略高。核心思路是:先反转head->next之后的链表,得到新头结点,然后把head接到链表尾部,最后把head->next置空。
ListNode* reverseListRecursive(ListNode* head) { if (head == nullptr || head->next == nullptr) { return head; } ListNode* newHead = reverseListRecursive(head->next); head->next->next = head; head->next = nullptr; return newHead; }笔试时如果时间紧张,我更推荐迭代法,不容易递归过深出错,而且边界条件只需要处理空链表和单节点两种情况。如果题目进阶成“每K个一组反转链表”,也是用迭代法加一个哨兵头节点最方便,这个变体很值得提前练一下,因为它经常被拿来当第二道编程题的铺垫。
3.2 最长递增子序列:从DP到二分优化的演进
动态规划是编程题的常客,2020年那套卷子编程题里出现了一道典型的DP题。如果让我押安全公司开发笔试的DP题型,最长递增子序列(LIS)出现的概率极高,因为它既能用朴素O(n^2)动态规划解,又能用贪心+二分优化到O(n log n),可以很好地区分应试者水平。
朴素DP思路:定义dp[i]表示以nums[i]结尾的最长递增子序列长度,初始值全是1。遍历每个i,再回头找所有j < i,如果nums[j] < nums[i],就更新dp[i] = max(dp[i], dp[j] + 1)。最终答案是dp数组的最大值。
int lengthOfLIS(vector<int>& nums) { int n = nums.size(); if (n == 0) return 0; vector<int> dp(n, 1); int res = 1; for (int i = 1; i < n; i++) { for (int j = 0; j < i; j++) { if (nums[j] < nums[i]) { dp[i] = max(dp[i], dp[j] + 1); } } res = max(res, dp[i]); } return res; }如果n的范围达到10^5量级,O(n^2)就会超时,这时候要用贪心+二分。维护一个tails数组,tails[len]表示长度为len的递增子序列中末尾元素的最小值。遍历nums时,在tails里二分找到第一个大于等于当前元素的位置并替换,如果当前元素比所有末尾值都大则扩展tails长度。这个做法能过的数据范围远大于朴素DP,笔试中有时间建议直接写优化版,能体现算法功底。
这类题的通用套路是识破“最长/最小/方案数”问题里的DP结构,先写出状态定义和转移方程,再考虑能不能优化。安全公司笔试的编程题通常不会上特别偏的模型,把LIS、编辑距离、背包问题这老三样练熟,基本覆盖90%的DP考法。
3.3 字符串处理实战:以“压缩字符串”为例
除了链表和DP,字符串处理也是安全公司笔试的固定方向之一,而且往往和安全编码有点关联,比如反转字符串、判断回文、实现字符串匹配、去除重复字符。这类题难度不大,但极其考验边界条件处理,一个++i和i++写反就得调试半天。
2020年试卷的编程题里有一道和字符串压缩类似的题。题目原型是:给定一个字符数组,把连续重复字符压缩成“字符+次数”的形式,要求原地修改并返回新长度。比如chars = ["a","a","b","b","c","c","c"],压缩后是["a","2","b","2","c","3"],返回6。
这道题有两个关键点:一是双指针法,一个指针遍历原始字符,一个指针写入结果位置;二是当连续字符个数大于等于10时需要转成字符串逐位写入,不能只写一个字符。后者是很多人丢分的地方,因为字符数组里只能逐字符存,数字10以上的场景很容易漏处理。
int compress(vector<char>& chars) { int n = chars.size(); int write = 0; int i = 0; while (i < n) { int j = i; while (j < n && chars[j] == chars[i]) { j++; } chars[write++] = chars[i]; int count = j - i; if (count > 1) { string cntStr = to_string(count); for (char c : cntStr) { chars[write++] = c; } } i = j; } return write; }这类题在笔试中最大的坑不是算法想不到,而是测试用例覆盖不全。“所有字符都不重复”“所有字符都相同”“次数超过9”这三个边界用例,提交之前一定要在脑子里过一遍。很多同学刷题时习惯了在线OJ自动判断,笔试时没有反馈,就必须靠自己的测试用例意识去补位。
4. 安全方向的隐藏分:为什么安全公司会考这些
4.1 路径遍历与输入校验:一道题看穿安全编码思维
安全公司的开发笔试和普通互联网公司最大的区别,就是它会考安全编码题。这类题通常不会单独出一道“请解释什么是SQL注入”,而是把安全思维揉进选择题、填空题甚至编程题里。热词搜索里“奇安信输入验证:路径遍历”这个方向,正好是这类考点的代表。
路径遍历(Path Traversal)的核心是:用户传入的文件名或路径没有被正确处理,攻击者可以通过../../这类序列跳出预期目录,读取敏感文件。比如程序要打开/var/data/目录下的文件,用户传入../../etc/passwd,如果代码直接把用户输入拼到路径后面,就会读到不该读的文件。
安全编码的做法是:先规范化路径,再校验最终路径是否仍在允许的目录范围内。用Java写的话,核心是调用Paths.get(baseDir, userInput).normalize().toString(),然后判断结果是否以baseDir开头。用Python写就是os.path.realpath后再startswith判断。但这里有个细节,startswith判断可能被/var/data_evil这种前缀绕过,所以校验时要在目录后面加上分隔符再比。
笔试中这类知识点的考法往往是选择题:“以下哪种写法可以有效防御路径遍历攻击?”选项里会有“过滤用户输入中的..”“使用白名单校验文件名”“先规范化路径再校验前缀”“直接把拼接好的路径交给系统调用”。正确答案是规范化+校验,单纯过滤..是典型的黑名单绕过点,攻击者可以用URL编码、双重编码等方式绕过。
这个考点背后的本质是:安全公司开发的每行代码都要默认“用户输入是不可信的”。普通开发写代码时假设用户是好人,安全产品开发写代码时假设用户是攻击者,代码里对输入的处理方式完全不同。平时刷算法题可能不太关注这个思维,但考安全公司之前一定要把你的代码视角切换过去。
4.2 栈溢出、格式化字符串与内存安全基础
除了输入校验,内存安全相关的概念也是安全公司笔试题里的常客。选择题可能会给一段C代码,问执行后是否会发生缓冲区溢出,缓冲区大小、拷贝长度哪个参数决定了风险。这类题背后考的是栈布局的基本概念:局部变量在栈上相邻排列,数组越界写入会覆盖相邻变量,如果覆盖到返回地址,程序流程就可能被劫持。
格式化字符串漏洞也是一类经典考点。选择题会给出printf(user_input)这样的代码,问有什么风险。答案是格式化字符串漏洞,攻击者可以用%s读取栈上任意地址的内存,或者用%n往任意地址写数据。正确写法是printf("%s", user_input),永远不要把用户输入当成格式化字符串的第一个参数传进去。
整数溢出在安全编码里也经常出现。比如malloc(strlen(input) + 1),如果strlen(input)返回一个很大的值,加1之后可能溢出变成很小的正整数,导致分配一块很小的缓冲区,然后memcpy拷贝大量数据进去,形成堆溢出。这类知识点在普通开发岗笔试里基本不会出现,但奇安信这类安全公司会考,因为它直接关系到底层产品代码的安全性。
我不是说每个投安全公司开发岗的人都得成为漏洞挖掘专家,但至少要知道这些基础漏洞的原理和防御手段。现在很多安全公司的笔试面试都会聊到OWASP Top 10,路径遍历、命令注入、SQL注入这些基础分类最好能用自己的话讲清楚。一个写业务代码的开发可以不懂攻击方法,一个写安全产品的开发如果也不懂,那写出来的产品自己都防不住自己。
5. 备考策略与实战心得
5.1 笔试现场的时间分配策略
基于这套卷子的结构和分值分布,我建议的答题顺序是:先花35到40分钟把选择、多选、填空全部做完,再留50到60分钟做编程题,最后10分钟检查。为什么先把客观题做完?因为客观题分值是确定的,做对就有分,编程题可能一道题卡住半小时也AC不了,如果先把时间耗在编程题上,前面的分数反而丢了。
做多选题要格外谨慎。多选少选多选都不得分,遇到不确定的选项宁可不选,也不要冒险。安全编码相关的多选通常只有一个“最正确”的组合,平时练习时就要养成“逐项判断对错”的习惯,而不是凭感觉圈选项。
编程题先读清楚题意再动手。很多同学看到题目很像LeetCode原题,兴奋地直接写之前背的答案,结果输入输出格式对不上,或者没注意到题目加了“必须原地修改”的限制。建议花两分钟把输入范围、返回类型、边界条件都画在草稿纸上,再开始写代码。
5.2 安全公司开发岗的刷题侧重点
如果目标是安全方向的开发岗位,刷题不能只盯着LeetCode Hot 100。我的建议是分成三条线并行走。
第一条线是经典算法题,重点刷链表、二叉树、字符串、动态规划、双指针。这些是笔试编程题的主要来源。LeetCode的Hot 100和剑指Offer覆盖得比较全,刷两遍以上,每道题都能独立写出来,编程题基本不会慌。
第二条线是计算机网络和操作系统基础题。重点背TCP状态迁移、HTTP状态码、死锁条件、进程调度算法、内存分区、用户态内核态切换。这些不止笔试要用,后面的技术面试还会被反复追问。刷题类App里也有专门的计算机基础题库,可以利用碎片时间过一遍。
第三条线是安全编码题。这一条线容易被忽视,但恰恰是奇安信这类公司的差异化考点。重点学习路径遍历、命令注入、SQL注入、XSS、CSRF、缓冲区溢出、格式化字符串漏洞的原理和修复方式。不要求能写Exploit,但至少要能识别不安全的代码写法,并能写出对应的安全版本。
我当时复习安全编码时用的办法是:每学一个漏洞类型,就自己写一段“有漏洞的代码”和一段“修好的代码”做对比,这个对比过程就是笔试多选题的答题逻辑:给你四种写法,判断哪些安全哪些不安全。
5.3 复盘之后的几条实在建议
最后聊几条实际操作层面的建议,都是我复盘这套卷子之后觉得最有价值的东西。
第一,一定要提前去牛客网刷几套该公司或同类安全公司的真题。笔试系统和LeetCode的手感差别很大,尤其是不支持本地IDE的在线编辑器,用起来会觉得自己手残。提前适应牛客网的代码编辑器和输入输出模板,可以避免考场上因为不熟悉环境浪费时间。
第二,C++方向的同学把智能指针和STL容器的底层实现过一遍。unique_ptr的移动语义、shared_ptr的引用计数、vector扩容机制,是选择题的高频素材。Java方向的同学把HashMap的put流程、ConcurrentHashMap的锁粒度演进、垃圾回收算法过一遍。这些内容的命中率很高,值得花时间。
第三,编程题如果没有思路,可以先写暴力解法,确保拿一部分分。很多在线判题系统是按测试用例给分的,暴力解法能过基础用例就有基础分,比空着强。如果暴力解法都写不出来,那就把思路和伪代码写在注释里,至少让阅卷人看到你的思路方向。
第四,填空题通常考输出结果,比如“请写出以下代码的输出结果”。这类题分值不高,但往往有两个坑:变量作用域和指针修改。遇到指针相关代码,画一张内存示意图,把每个变量的地址和值标出来,正确率会大幅提升。这一步看上去费时间,实际上比心算可靠得多。
写在最后
我个人的体会是,安全公司开发岗的笔试,更大的价值不在于那两道编程题,而在于它帮你建立了一种“写代码前先想攻击面”的思维方式。考完这套卷子之后,我再看自己以前写的代码,感受完全不一样了。以前写文件上传接口只想着格式校验,现在会想用户传入文件名能不能带路径分隔符,目录拼接后会不会绕过前缀判断,异常分支里会不会把堆栈信息泄露出去。这种视角的转变,比多刷一百道算法题更重要。
如果你正在准备奇安信或者其他安全厂商的开发岗,不妨找几套真题先感受一下,别急着刷难题。把基础数据结构、操作系统、网络、安全编码这几条线拉平,比任何单项突出都管用。笔试只是第一关,但第一关的筛选逻辑已经告诉你,这个岗位需要什么样的人:基础扎实、代码素养高、脑子里始终装着安全和边界。