前言
自己有做一个机器人框架:https://github.com/project-yui
一直以来都是使用官方客户端启动框架,但这也存在问题:官方客户端包含electron的一些组件,无用库比较多;
昨天,突发奇想,想着尝试一下使用nodejs启动。
分析
根据之前开发框架经验,入口是wrapper.node
,直接编一个test.js看什么反应:
// test.js const wrapper = require('./wrapper.node') console.info('wrapper:', wrapper)
以下是输出,看起来再尝试加载,但是有东西没找到,那就补一下看看。
$ node test.js node:internal/modules/cjs/loader:1865 return process.dlopen(module, path.toNamespacedPath(filename)); ^ Error: /xxxxxxxxx/program/libbugly.so: undefined symbol: gnutls_free at Object..node (node:internal/modules/cjs/loader:1865:18) at Module.load (node:internal/modules/cjs/loader:1441:32) at Function._load (node:internal/modules/cjs/loader:1263:12) at TracingChannel.traceSync (node:diagnostics_channel:322:14) at wrapModuleLoad (node:internal/modules/cjs/loader:237:24) at Module.require (node:internal/modules/cjs/loader:1463:12) at require (node:internal/modules/helpers:147:16) at Object.<anonymous> (/xxxxxxxxx/Yui/program/test.js:1:17) at Module._compile (node:internal/modules/cjs/loader:1706:14) at Object..js (node:internal/modules/cjs/loader:1839:10) { code: 'ERR_DLOPEN_FAILED' } Node.js v22.20.0
编一个preload.so文件,加载guntls;
extern "C" { #if defined(__linux__) || defined(__APPLE__) // Ensure gnutls_xxx is linked in void * _force_gnutls_global_init __attribute__((used)) = (void *)gnutls_global_init; #else #endif }
使用LD_PRELOAD=preload.so启动node,可以看到报错发生了变化:
$ LD_PRELOAD=/xxxxxx/preload.so node test.js node: symbol lookup error: /xxxxxxx/program/wrapper.node: undefined symbol: qq_magic_napi_register
实现一个空函数看看:
// Keep the existing registration helper void qq_magic_napi_register() { spdlog::info("call qq_magic_napi_register"); }
编译执行,报错变了,提示没用自己注册,看来此函数实在注册模块,使用ida看看怎么注册的。
$ LD_PRELOAD=/xxxxxx/preload.so node test.js [2025-10-05 16:14:56.976] [info] call qq_magic_napi_register node:internal/modules/cjs/loader:1865 return process.dlopen(module, path.toNamespacedPath(filename)); ^ Error: Module did not self-register: '/xxxxxxx/program/wrapper.node'. at Object..node (node:internal/modules/cjs/loader:1865:18) at Module.load (node:internal/modules/cjs/loader:1441:32) at Function._load (node:internal/modules/cjs/loader:1263:12) at TracingChannel.traceSync (node:diagnostics_channel:322:14) at wrapModuleLoad (node:internal/modules/cjs/loader:237:24) at Module.require (node:internal/modules/cjs/loader:1463:12) at require (node:internal/modules/helpers:147:16) at Object.<anonymous> (/xxxxxxxx/program/test.js:1:17) at Module._compile (node:internal/modules/cjs/loader:1706:14) at Object..js (node:internal/modules/cjs/loader:1839:10) { code: 'ERR_DLOPEN_FAILED' } Node.js v22.20.0
看起来是napi_module_register
换了个名字,那就引入napi调用它;
代码如下:
extern "C" { // Keep the existing registration helper void qq_magic_napi_register(napi_module *m) { spdlog::info("call qq_magic_napi_register"); napi_module_register(m); } #if defined(__linux__) || defined(__APPLE__) // Ensure gnutls_xxx is linked in void * _force_gnutls_global_init __attribute__((used)) = (void *)gnutls_global_init; #else #endif }
编译执行,看到执行成功没有报错:
$ LD_PRELOAD=/xxxxxx/preload.so node test.js [2025-10-05 16:22:27.940] [info] call qq_magic_napi_register wrapper: { NodeIQQNTWrapperEngine: [Function: NodeIQQNTWrapperEngine], NodeIKernelThirdPartySigService: [Function: NodeIKernelThirdPartySigService], NodeIKernelECDHService: [Function: NodeIKernelECDHService], NodeIKernelLoginService: [Function: NodeIKernelLoginService], NodeIOPSafePwdEdit: [Function: NodeIOPSafePwdEdit], NodeIQQNTWrapperSession: [Function: NodeIQQNTWrapperSession], NodeIBatchUploadManager: [Function: NodeIBatchUploadManager], NodeIKernelBatchUploadService: [Function: NodeIKernelBatchUploadService], .......
最后使用更优雅的方式处理,在框架里面加代码:
if (process.platform === 'linux') { log.info('preload.node') const m = { exports: {} }; const preload = process.env['YUI_PRELOAD'] || resolve(__dirname, './preload.node') process.dlopen( m, preload, constants.dlopen.RTLD_NOW | constants.dlopen.RTLD_GLOBAL ) }
后面使用框架测试,能正常登录账号,机器人工作正常。
总结
有点难度,需要知道LD_PRELOAD的使用,ida分析,以及快速识别napi函数。
现在,Linux下不用依赖官方客户端启动了,哈哈哈。