绿盟科技 软件研发 秋招一面:gdb 排查与系统安全基础
- 轮次
- 一面
- 时间
- 2026-09
- 来源
- 牛客网
《面试题目》
- gdb 中如何定位一个线上 C++ 进程偶发崩溃的问题?
- 对称加密和非对称加密有什么区别?实际系统中如何组合使用?
- 共享内存、消息队列和 Unix Domain Socket 分别适合什么场景?
- HTTPS 建立连接时经历了哪些关键步骤?
- 哈希表底层如何实现?为什么负载因子过高会导致性能下降?
- Linux 中常见的 I/O 模型有哪些?多路复用解决了什么问题?
- 如何设计一个可靠的设备任务下发系统,避免任务重复执行?
- C++ 中移动语义解决了什么问题?什么时候移动仍然可能发生拷贝?
- 请先做一下自我介绍,并重点介绍你在项目中负责的部分。
《参考解析》
线上偶发崩溃:先用 core 定死现场,再用 Sanitizer 复现:排查顺序是「确认信号类型 → 找到崩溃指令 → 判断内存破坏还是并发问题 → 用插桩版复现」。前提是 core 真的落了盘:ulimit -c unlimited(或 systemd 的 LimitCORE=infinity)、sysctl kernel.core_pattern(以 | 开头表示交给 systemd-coredump 之类的 handler,此时用 coredumpctl list / coredumpctl gdb <pid> 取,文件在 /var/lib/systemd/coredump/;容器里要 --ulimit core=-1 且宿主 core_pattern 不能指向容器内不存在的程序)。分析时必须用与线上同一次构建的二进制:比对 readelf -n app | grep 'Build ID',符号被 strip 就 objcopy --only-keep-debug 出的 .debug 配合 .gnu_debuglink,或依赖 debuginfod,动态库用 set solib-search-path 指过去——版本对不上时 gdb 只给一句 “core file may not match” 的警告,栈全是错的。常用动作:gdb -q app core,set pagination off,thread apply all bt full(偶发崩溃经常是别的线程把内存写坏,只 bt 当前线程会看不到真凶),info threads 轮询各线程,info registers、x/8i $pc-16 看崩溃指令,p $_siginfo 读 si_code(SEGV_MAPERR=1 访问未映射地址,SEGV_ACCERR=2 权限不符)和 si_addr。栈被踩坏时 bt 会断在 ??,这时改看当前帧的寄存器、x/16gx $rsp 的栈内存布局,反查是谁越界写了这块。-O2 下局部变量常显示 optimized out,别把它当结论,用 -Og 或看反汇编。定位不到就用插桩版复现:-fsanitize=address,undefined -fno-omit-frame-pointer -g -O1(编译和链接都要带同样的 flag),跑压测或回放流量;ASan 抓越界/释放后使用,约 2 倍耗时、2~3 倍内存,ASAN_OPTIONS=abort_on_error=1:disable_coredump=0:log_path=/var/log/asan;UBSan 用 UBSAN_OPTIONS=print_stacktrace=1:halt_on_error=1。怀疑数据竞争或死锁要单独用 -fsanitize=thread 编一版(TSan 不能和 ASan 同时开),死锁则先在 gdb -p <pid> 里 thread apply all bt 看是否集体卡在 pthread_mutex_lock(attach 需要 ptrace_scope 允许或 CAP_SYS_PTRACE)。线上不方便 attach 时,SIGSEGV handler 里用 backtrace() + backtrace_symbols_fd() 写日志是兜底,但只能用 async-signal-safe 的调用,别再 malloc。
混合加密:非对称只用来协商,对称用来搬运数据:对称加密(AES-GCM、ChaCha20-Poly1305)加解密同一个密钥,走 AES-NI 能到 GB/s 量级,问题是密钥怎么安全送到对端;非对称加密(RSA、ECDSA/ECDHE)公私钥分离,可做密钥交换、身份认证和签名,但 RSA-2048 一次私钥运算就是毫秒级,比对称慢几个数量级,且 RSA 单次能加密的明文受模数限制(2048 位配 OAEP-SHA256 只有 190 字节),根本不适合加密业务数据。工程上的标准组合是:ECDHE(X25519 或 P-256)协商出共享秘密,HKDF 派生出会话密钥,业务数据走 AES-256-GCM,身份靠证书 + 数字签名(RSA-PSS 或 ECDSA)验证;ECDHE 的私钥每次会话现生成,长期私钥泄露也不解历史流量,即前向保密。AES-GCM 最要命的约束是同一密钥下 nonce 绝不能重复:GCM 的 nonce 重复会让攻击者恢复 GHASH 的认证密钥 H,进而伪造任意密文、拿到认证通过,属于灾难级。所以 nonce 不要纯随机撒——推荐 4 字节固定前缀 + 8 字节单调计数器(每加密一次自增),或者干脆每次会话换一把新 key;用随机 96 位 nonce 时,量级上到 2^32 条消息就有生日碰撞风险,同一密钥加密总量也别超过约 64 GiB(2^39-256 bit 的规范上限)。另外要区分签名和加密:签名证明来源与完整性、不可抵赖,不隐藏内容;加密保证机密性,不证明是谁发的,两者不能互相替代。别用 ECB、固定 IV、PKCS#1 v1.5 加密这类过时做法。
共享内存 / 消息队列 / Unix Domain Socket 选型:三者本质是「拷贝次数 × 同步复杂度 × 消息语义」的取舍。共享内存用 shm_open + ftruncate + mmap(MAP_SHARED)(或 memfd_create),数据只在生产者和消费者各自 mmap 的那一次写入里进出,中间零内核拷贝,吞吐能顶到内存带宽,适合高频大块数据(日志、行情、图像帧);代价是内核完全不提供同步和消息边界,得自己配信号量、互斥量或环形队列。消息队列(POSIX mq_open / System V msgget,或 Kafka、Redis 这类外部队列)天然有消息边界和优先级,能解耦生产消费、削峰填谷,适合可靠的小结构化任务投递;坑在单条消息大小和队列深度的内核上限(受 msg_max、msgsize_max 限制)、积压、以及消费者异常退出后的重复投递。Unix Domain Socket 是 AF_UNIX,SOCK_STREAM/SOCK_DGRAM/SOCK_SEQPACKET 三选一,SOCK_SEQPACKET 兼有可靠与保留边界,接口和网络 socket 一样所以改动成本低,同机吞吐明显高于 TCP 回环(省掉校验和与协议栈),还能用 SCM_RIGHTS 传文件描述符、SO_PEERCRED 拿对端 uid/pid 做鉴权,适合控制命令、状态通知、本地 RPC。经验口径:大块高频数据走共享内存环形缓冲,控制面走 UDS,需要可靠异步投递和跨机扩展走消息队列。共享内存无锁环形队列的写法要落到细节:SPSC 场景用两个 std::atomic<size_t> 记单调递增的写/读游标,容量取 2 的幂、下标用 pos & (cap-1),空判定 read == write、满判定 write - read == cap(用单调计数而不是取模后的值,就不用区分「满」和「空」);生产者先写数据再 store(write, memory_order_release),消费者先 load(write, memory_order_acquire) 再读数据,靠这对 acquire/release 建立 happens-before;多生产者要么用 CAS 抢槽位,要么每生产者一条子队列。两个 atomic 各自 alignas(64) 分到不同 cache line,避免伪共享;批量提交一次发布游标,比逐条发布快得多。volatile 不能替代 atomic——它既不保证原子性也不建立内存序。
TLS 握手与证书校验:TLS 1.3 是 1-RTT:客户端 ClientHello 带上 supported_versions、密码套件、SNI、ALPN 和 key_share(通常是 X25519 公钥);服务端回 ServerHello 选定参数并给出自己的 key_share,此后的 EncryptedExtensions、Certificate、CertificateVerify、Finished 全部加密发送;客户端验完证书、发 Finished,双方用 HKDF 从 ECDHE 共享秘密派生出握手密钥和应用流量密钥(client_application_traffic_secret_0 等),记录层一般用 AES-128-GCM 或 ChaCha20-Poly1305,nonce 由静态 IV 异或记录序列号得到,协议自己保证不重复。TLS 1.2 需要 2-RTT,且允许 RSA 密钥传输、CBC 之类已被淘汰的套件。客户端校验服务端证书至少四步:有效期(注意本机时钟偏差)、SAN 里的域名匹配(CN 已废弃)、证书链能否逐级验签到本地信任根(openssl verify -CAfile ca.pem cert.pem,中间的 CA 证书要 BasicConstraints: CA:TRUE、KeyUsage: keyCertSign)、以及签名算法与吊销状态(CRL/OCSP,浏览器多为软失败)。排查「HTTPS 慢」别一口咬定加密慢,把耗时拆开测:curl -w '\nDNS: %{time_namelookup}\nConnect: %{time_connect}\nTLS: %{time_appconnect}\nTTFB: %{time_starttransfer}\nTotal: %{time_total}\n' https://example.com,其中 time_appconnect 是 TLS 完成时刻,它比 time_connect 多出来的才是握手开销;握手本身要细看用 openssl s_client -connect host:443 -servername host -tls1_3 -state -timeout 5。常见真凶是 DNS、TCP 建连、证书链不全(漏发中间证书导致客户端额外去下载)、连接池没复用每次重握手(可开 session ticket / TLS 1.3 PSK 复用)、以及服务端排队。
I/O 多路复用与 epoll 的边沿触发:五种模型里,阻塞 I/O 让线程睡在 read 上,非阻塞 I/O 要靠应用自己轮询,信号驱动 I/O 用 SIGIO 通知(实际很少用),I/O 多路复用用一个线程等一批 fd,异步 I/O(io_uring、POSIX AIO)才是内核把数据搬完再通知。多路复用解决的就是「C10K 下不能一连接一线程」的问题:select 有 FD_SETSIZE(默认 1024)上限,每次调用都要把 fd 集合从用户态拷进内核并线性扫描;poll 去掉了数量限制但仍要遍历全部 fd;epoll 在内核用红黑树维护监听集合、用就绪链表返回事件,epoll_wait 的返回只跟活跃 fd 数量相关,所以适合「连接多、活跃少」。epoll_create1(EPOLL_CLOEXEC) + epoll_ctl(ADD/MOD/DEL) + epoll_wait 之外,真正容易翻车的是触发模式:LT(默认)只要缓冲区还有数据就反复通知,写起来不容易错;ET 只在状态变化时通知一次,必须把 fd 设成非阻塞并循环 read 到返回 -1 且 errno == EAGAIN/EWOULDBLOCK,否则剩下的数据再也不会触发事件,多线程下更糟——一个线程读了一半,别的线程永远等不到通知。多线程同抢一个 fd 可以用 EPOLLONESHOT,事件投递后自动失效,处理完再 epoll_ctl(MOD) 重新武装,保证同一时刻只有一个线程持有。写侧同样要处理 EAGAIN:注册 EPOLLOUT、挂上用户态发送缓冲,写完再摘掉 EPOLLOUT,否则空转会烧满 CPU。另外 epoll 只告诉你「现在有事」,不保证一次读完,所以非阻塞 fd、半包状态机(长度前缀或分隔符)、EPOLLRDHUP 与 read 返回 0 的关闭处理,必须一起设计。
设备任务下发的幂等:核心是把「至少一次投递」和「幂等执行」拼成事实上的恰好一次。每个任务带全局唯一 task_id(服务端生成,同时带设备 ID 与单调递增的 version),设备侧把执行记录持久化(本地 SQLite / 带唯一约束的表),收到任务先按 task_id 查记录:已成功就直接回传历史结果,执行中就回「处理中」,查无此任务才真正执行——这一步是去重的关键,不能只靠内存里的 Set,重启就丢。服务端的状态机至少要有 PENDING / SENT / ACKED / RUNNING / SUCCESS / FAILED / TIMEOUT,并且绝不能把「发送成功」当成「执行成功」:TCP 写成功只代表本机缓冲区收下了,设备的回包可能丢,必须靠设备侧状态上报、超时查询和重试把状态收敛回来。同一任务被多个调度器抢时,用条件更新拿执行权:UPDATE device_task SET status='RUNNING', version=version+1 WHERE task_id=? AND status='PENDING' AND version=?,只有影响行数为 1 的那个调度器真正下发;跨进程也能用 Redis SET key val NX PX 30000、etcd lease 或 ZooKeeper 临时节点,但要配 fencing token——锁过期后原持有者可能还在跑,写操作带上递增的 token 让存储层拒绝过期持有者。重试必须有上限和退避(例如 1s、2s、4s、8s、16s,加随机抖动防雪崩),超过次数转人工或告警;设备侧按 version 丢弃过期任务,防止乱序到达的旧指令覆盖新状态。最后,业务执行本身最好也幂等(结果用 result_hash 落库、同样输入重跑不改状态),这样重试才是安全的。