个人技术演进线路(不定期更新)【交叉编译|终端安全|驱动开发|EDR|性能调优|鸿蒙开发】

最近看新机会,故整理一份职业生涯自述,看看有没有气场相合的团队。
下面主要按照笔者的技术演进及时间线,聊聊这些年折腾过的一些底层技术和填过的坑。
最后更新时间 2026.09.02

0x00 驱动力:好奇心 && 兴趣

和大部分 C/C++/Qt 开发者一样,笔者早期也着重于常规的业务开发。但又属于那种只要时间充裕‘就想看看底层原理’的性格,比起堆叠业务代码,更喜欢去向下拆解系统机制,这也促使笔者一步步走向了系统底层和安全的深水区。

0x01 交叉编译:从“造轮子”到“魔改 ELF”

  • 从 ARM 到手搓 MIPS64el 工具链:
    • 最初接触交叉编译是从折腾 ARM 开发板开始。
      • GitHub:嵌入式学习记录 (该仓库建立于 23 年底学习 uboot 时,学习裸机程序则是在 21 年初)
      • 后来在工作中为了简化工作流,在不知道社区有现成轮子的情况下,通过源码摸索编译出了 MIPS64el 交叉工具链。虽然这套轮子最后没投入生产,但让我了解到了一些底层硬件编译参数,并在 24 年借此帮同事精准定位了一个由浮点运算引起的崩溃。
  • Qt 工具链:
    • 为了实现底层服务与 UI 界面在同一套流水线下的协同编译,疯狂线上冲浪,记得最后是从 Stack Overflow 找到了一篇关于 Qt4 交叉编译的古早线索,随后通过不断摸索,试错,取巧,成功搓出了 Qt5 交叉编译工具链,打通了前后端同构编译的闭环。
  • 国产架构溯源与三方库兼容:
    • 早期深入研究了国产化架构的发展演进史。后期在遇到部分开源三方库不支持国产芯片架构交叉编译时,利用“架构溯源”的思路——将目标架构强制指定为其前身,成功绕过了很多棘手的编译限制。
  • 魔改 ELF 绕过 GLIBC 依赖:
    • 在将出包平台规模化部署到一台 CentOS 6 老机器时,为了让基于高版本 GLIBC 构建的工具链能在低版本系统上运行,我没有使用妥协性的环境变量或 rpath ,而是直接通过修改已编译好的 ELF 文件实现了向下兼容。这让我对 ELF 文件结构和 GLIBC 版本校验逻辑形成了深刻的肌肉记忆,也为后期开发内核模块适配工具埋下了伏笔。
  • 旧世界生态架构跃升:
    • 针对 CentOS 6 默认 GCC 4.9.x 对 C++11 支持不完整、C++ ABI std::string 版本冲突以及现代三方库的依赖断代问题,我对交叉编译平台进行了一次底层大升级。将其整体对齐到了申威与龙芯“旧世界”的 8.3 版本,不仅完美支持了 C++17 特性,也保证了编译产物对老旧系统的兼容性,为整个项目的现代化平滑过渡打下了基石。

0x02 逆向与攻防:代码注入

  • 这原本是本人在职期间找工作的过程中,深感底层与安全攻防圈子更看重纯粹的“真实战斗力”而学历其次,动了转行的念头,然后就对两者都涉及的 ShellCode 产生了浓厚兴趣并进行了学习,最初也没料到,这竟然为后来业务上实现基于内核热补丁( LivePatch )原理的 inline hook 埋下了伏笔,那时也不知道两者技术底色一脉相承😑。

0x03 主防平台:从“应用层”到“内核态”

  • 针对主防平台,我们的需求是“接收事件 -> 规则过滤&行为分析 -> 杀毒引擎 -> 决定是否放行”。基于上述需求,我研究并采集了多种 Linux 应用层 + 内核态 的监控方式并实现了两套。最初因为驱动方案需要逐一编译,对应用层方案不死心,也消耗了一定的时间。由于陈述出来又太多了,故详见笔者另一篇文章。

0x04 性能调优:从“内核态”到“应用层”

  • 在主防平台搭建结束后,便开始着手优化性能。
  • 消除惊群效应
    • 最初的唤醒挂起中的进程是完全不分桶的,当上层抛送来消息后,唤醒的是所有进程,然后每个进程检查是否是给自己的消息,惊群效应十分严重,不过此状态持续时间很短,在驱动完全开发之前就已经实现了依据监控点的分桶唤醒机制。
    • 但各个监控点的“冷热”程度是不同的,有些监控点可能每秒被触发1000次,而有些监控点10秒触发一次,所以后面又想到依据进程信息再划分一层桶,就演变成了进程信息+监控点的双层分桶或者说是分片+分桶逻辑吧。
    • 再后面觉得分片加分桶依旧不够极致,然后去查寻等待队列相关接口文档,看有没有更妥善的方法,结果发现等待队列支持等待者设置回调,用于唤醒者检查是否满足自身唤醒条件再唤醒,可以避免掉无谓的进程调度开销!完美!!!
  • 消除瓶颈效应
    • 随着驱动的不断演化,由驱动抛往上层的消息逐渐变成了可变长数据结构,这就导致上层需要先从netlink套结字中嗅探数据长度,然后再去读取数据,并发读取需要加锁。凭借经验,认定此处必定会产生瓶颈效应,于是实现了仅保留上下层通信的最小化测试用例,使用perf追踪确认,果然验证了我的猜想。
    • 后面便琢磨去掉这个锁,但如果去掉锁,在读取可变长数据结构的场景下,必须要每个线程都有自己的读缓冲区才行;同时,底层抛上来的数据有自己的消息头,从驱动向上发送消息时,需要netlink进行消息包装,把数据拷贝到自己的负载中带上自己的消息头,而netlink的头部信息对我根本没用,所以干脆彻底换一种通信方式似乎更治本。
    • 随后就查阅内核态与应用层的各种通信方式,发现了DPDK这种高性能通信方式,查阅资料得知其核心实现是基于设备文件的、核心绑定的、全链路分片通信方式,为了避免DPDK比netlink更甚,带入一堆我用不到的东西,且后续可能存在从内核3.10到6+的兼容性问题,所以笔者就借鉴DPDK核心思想,利用设备文件+环形缓冲区+内存屏障+绑核+epoll通知,实现了一套轻量级的通信逻辑,消除了瓶颈效应。
    • 当优化到上面这一步,笔者强迫症又犯了,突然想到,如果内核中使用CPU0写入0号缓冲区,应用层使用CPU0读取0号缓冲区,而不是CPUx随机读写某个缓冲区,岂不是连内存屏障都省了?当准备开始尝试时突然又想到以前遇到过的内核软死锁、以及一些特殊场景下某个CPU一直被占用的问题。罢了……目前这套方案或许已经是在性能和可用性之间的最佳权衡了。
  • 在驱动性能调优的过程中,除了上述使用到的各类机制,过程中笔者也了解了 RCU 锁及其底层原理,于是又将其与内存屏障的思想应用到了应用层的性能调优过程中。下面是一个典型的配置管理器(单例)代码片段用于“过滤策略”的更新,具体如下所示:
class ProductMgrNotLockMgr
{
public:
    ProductMgrNotLockMgr()
        : m_productMgr(nullptr), m_productMgrVer(0){}
    ~ProductMgrNotLockMgr(){}
    shared_ptr<ProductMgr> getProductMgr()
    {
        static thread_local uint64_t m_productMgrVerTls = 0;
        static thread_local shared_ptr<ProductMgr> m_productMgrTls = nullptr;
        uint64_t globalVer = m_productMgrVer.load(memory_order_acquire);
        if(m_productMgrVerTls != globalVer)
        {
            m_productMgrTls = atomic_load_explicit(&m_productMgr, memory_order_acquire);
            m_productMgrVerTls = globalVer;
        }
        return m_productMgrTls;
    }
    void setProductMgr(shared_ptr<ProductMgr> mgr)
    {
        atomic_store_explicit(&m_productMgr, mgr, memory_order_release);
        m_productMgrVer.fetch_add(1, memory_order_release);
    }

private:
    shared_ptr<ProductMgr> m_productMgr;
    atomic<uint64_t>       m_productMgrVer;
};
  • 业务层中的每一个事件对象调用 get 方法取走自己的过滤策略,而更新时仅需要初始化新的策略配置,然后调用 set 即可,旧的策略配置在引用计数清零后会自动释放,至于策略配置为什么叫 ProductMgr 而不是 ConfigMgr 这属于历史遗留问题,不要在意这些细节🤭。
  • 其中的内存屏障部分:
    • memory_order_acquire 等同于内核中的 smp_rmb ,用于刷新失效队列,保证数据最新。
    • memory_order_release 等同于内核中的 smp_wmb ,用于保证指令执行顺序、刷新写缓冲区并通知。
  • 其中的 RCU 锁部分 本质就是对智能指针的原子替换过程:
    • 内核中的 RCU 锁需要写者阻塞等待最后一个读者结束对老内存的使用然后手动释放,而此处巧妙利用 C++ 的智能指针,不需要写者等待。
  • 其中的 static 关键字是为了延长局部变量的生命周期。
  • 其中的 thread_local 关键字是为了让每个线程都有自己的变量副本,避免缓存竞争。
  • 其中的 VerTls 变量,则是为了进一步提高性能,毕竟 shared_ptr 是个对象,内部存在多个变量。

0x05 鸿蒙原生拓荒

  • 近几个月主导了底层服务与 UI 向鸿蒙系统的全面移植适配。在这个文档稀缺的“拓荒期”,填平了大量底层生态差异的坑。
  • 生态差异:
    • 和 Linux 不同,鸿蒙中的C、C++开发必须严格遵循 POSIX 标准,Linux 常用的 GNU扩展接口 均无法正常使用。
    • 通常为了高兼容性与平台一致性,常用 gcc 作为编译器,而 鸿蒙SDK 携带的是更现代的、基于 musl libc 的 clang 编译器。
    • 在鸿蒙中,一个完整的应用,往往需要Ark TypeScript代码层 + C++代码层实现,因为有些能力 C++库 并不提供,这也是和常见的桌面端应用开发不一样的地方。
    • 上下层分离问题,以往上下层分离往往是依据进程分为人机交互界面和底层服务,而鸿蒙中一个应用就是一个进程,且不允许私自创建进程,故只能通过线程进行上下层分离。
    • 在 Linux 系统中开发,往往需要关注 root 权限问题,将需要高权限的行为下放到底层高权限服务中实现,而鸿蒙有自己的一套权限体系,申请了某个权限就允许你调用相关的接口。
    • 最后,区别于常规的 Linux 应用开发,实现某个功能可以有大量的方案,非常自由;鸿蒙属于面向框架的开发,实现某个功能往往只有那几个固定的接口,本人在此之前从没做过移动端开发,或许安卓、iOS开发也是如此吧。
  • 进行的工作:
    • 将代码 POSIX 化、 将界面、服务的库化、线程化,将原有的可执行程序包装为线程再编译成库,然后导出启停符号……、移除所有system 、popen 、execve 家族调用。
    • 对于原有代码的复用,则是实现了 桥接库 以及 Ark 实现层,部分业务在桥接库中使用鸿蒙提供的 C++ 库实现,另一部分穿透桥接库,使用 Ark TypeScript代码层 实现。
    • 对于界面从 Linux 平台的移植,本人选用了 GitCode社区 的版本,没有使用 QT 官方维护的版本,原因是行为差异较大,很多行为与 Linux 系统中并不一致,虽然社区版本也有行为差异,但整体可接受。
    • 关于 QT 与 Ark 画布的转接,GitCode 和 QT官方 的 Qt ,在画布转接上完完全全是两套不同的逻辑……,最后是配合 Ai 边猜边试实现的,记得一遍遍调整得(děi)有十几二十次吧😹。
    • 关于出包,鸿蒙当前只提供了 Windows 和 Mac 的教程,于是结合前两者摸索了一套 Linux平台 的出包模板,使用 SDK 中携带的工具出包、使用 java 进行签名、然后配合自动化出包脚本得以实现。
  • 后来者如果团队人员充沛,建议使用进行 Ark UI+ TypeScript 原生开发,原因是无论官方还是社区的Qt,在目前(2026.09)都会有大大小小的兼容问题,如果项目不大,处理这些兼容问题的时间做本地化开发也足够了,可以把更多的精力放在业务层实现上。

0x06 问题处理能力

  • 除了上述开发过程中遇到的各种问题外,还处理过其它的形形色色的问题,展开讲又很多,故一笔带过:
    符号表冲突、暴力隐藏了不该隐藏的符号(编译器 bug )、ELF 页对齐问题、代码不规范导致唯独龙芯(mips64el+loongarch64)Release 包进程崩溃、优化级别引起的汇编指令错误、早期三方库不支持国产化架构编译问题、C++不同 std 库的正则表达式行为不同问题、Qt 插件装载问题、冷门信创系统与 Qt 兼容性问题、各类资源泄漏问题……

0x07 结语

  • 以上除了主防的上层服务和鸿蒙的业务层代码适配两项,有同事配合我进行实现之外,其余绝大部分都是自己独立做出来的。
  • 目前笔者感兴趣的行业有:
    • 终端安全行业(我的老本行)
    • 性能调优领域
    • 逆向与攻防
    • 嵌入式领域
  • 文章仅发布于博客园 【禁止转载】

posted on 2026-08-22 18:17  书生执笔画浮沉  阅读(187)  评论(0)    收藏  举报

导航

 
途虽修远,非庸人之可企及            运虽多舛,非志士之所屈从