news 2026/10/20 4:55:03

通达信DLL编程实战:一机一码授权与机器码绑定

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
通达信DLL编程实战:一机一码授权与机器码绑定

简介:本资源面向通达信公式开发者与量化爱好者,聚焦在通达信公式中通过DLL插件调用实现一机一码的完整工程示例。包内共13个文件,以6个cpp源文件、4个h头文件为主,另含Visual Studio项目文件与一份通达信DLL函数编程规范文档,压缩包约88KB。源码覆盖插件功能测试、自定义函数注册、计算功能实现、磁盘信息获取以及MD5加密等模块,可帮助读者理解如何将唯一标识码生成逻辑嵌入通达信公式并完成激活验证与许可证管理。项目文件可直接在VS中打开编译调试,配合规范文档能快速理清接口定义与注册流程。目前已有2427人学习,适合具备C++基础、希望掌握通达信DLL插件开发与安全校验思路的中高级用户参考。

1. 通达信DLL编程到底能干什么:从公式里调出机器码说起

如果你写过通达信公式,大概率遇到过这种尴尬:公式语言能算均线、能画指标,但一旦想读硬盘序列号、想调外部加密接口、想做联网校验,公式编辑器立刻变成一堵墙。通达信DLL编程就是在这堵墙上开的一扇门——把 C/C++ 编译出来的动态库挂进公式系统,让公式里能像调MA(CLOSE,5)一样调你自己写的函数。而“一机一码”是这扇门后面最常见的诉求:指标或选股公式只授权给特定机器,换台电脑就失效。这套资源适合两类人:手里有自研指标想加授权保护的策略开发者,以及想把外部计算结果喂进通达信的老手。它不解决行情源问题,也不碰交易通道,只专注一件事——让公式能调用 DLL,并在调用时完成机器绑定校验。

2. 通达信DLL编程的接口约定与最小可运行工程

2.1 先搞清楚通达信认什么样的 DLL

通达信对 DLL 的调用不是 COM,也不是 .NET 托管程序集,而是一套非常朴素的 C 导出函数约定。它要求 DLL 导出特定名称的函数,公式里通过"DLL名称@函数名"的形式调用。常见做法是导出一个计算函数和一个数据函数,计算函数负责返回指标数值,数据函数负责返回字符串或额外数据。这里有个血泪经验:通达信主程序是 32 位的,所以你编译的 DLL 必须是 32 位,用 64 位编译器编出来的东西挂上去,公式编辑器连报错都懒得给你,直接静默失败。我一般会在工程属性里把平台锁定为 Win32,字符集用多字节,避免宽字符在公式字符串传递时出现玄学乱码。

导出函数的签名各家资料写法略有差异,但核心参数绕不开这几样:数据个数、输入数据数组、输出数据数组、参数数组。下面是一个最小可运行的骨架,只做一件事——把输入数据原样加一后输出,用来验证链路是否打通。

// minimal_tdx.c // 编译为 32 位 DLL,导出名为 TDX_CALC #include <windows.h> // __declspec(dllexport) 让函数出现在导出表里 // __stdcall 是通达信调用约定里最稳的一种,别用 cdecl 赌运气 __declspec(dllexport) int __stdcall TDX_CALC( int nDataLen, // 输入数据个数 float* pfIn, // 输入数据数组,比如 CLOSE float* pfOut, // 输出数据数组,由 DLL 填充 int nParamCount, // 参数个数 float* pfParam) // 参数数组 { // 防御性判断,通达信有时会传 0 长度进来 if (nDataLen <= 0 || pfIn == NULL || pfOut == NULL) { return -1; } for (int i = 0; i < nDataLen; i++) { // 原样加一,仅用于验证调用是否成功 pfOut[i] = pfIn[i] + 1.0f; } return 0; // 返回 0 表示计算成功 }

这段代码的逻辑说明:nDataLen是通达信传进来的 K 线根数,pfIn通常对应公式里传入的序列,pfOut是你需要填满的输出序列,长度必须和输入一致。参数说明:nParamCount和pfParam用来接收公式里写的参数,比如TDX_CALC(CLOSE, 10)里的 10 就会出现在pfParam[0]。返回值约定各家实现不同,有的用 0 表示成功,有的用 1,建议在自己的 DLL 里统一,并在公式测试时先用一个简单指标验证,别一上来就塞复杂逻辑。

编译命令用 MSVC 的话大致是这样,注意/DLL和 32 位目标:

cl /LD /Fe:minimal_tdx.dll minimal_tdx.c /link /MACHINE:X86

编译完把 DLL 丢到通达信安装目录的T0002\dlls下(不同版本目录名可能略有差异,以你本机为准),然后在公式里写:

TDX_CALC: "minimal_tdx@TDX_CALC"(CLOSE);

如果指标线出来了,说明 DLL 调用链路通了。这一步看着简单,但它是后面所有一机一码逻辑的地基,地基没打牢,后面全是空中楼阁。

2.2 公式侧怎么传参、怎么接返回值

公式侧调用 DLL 的语法是"DLL文件名@导出函数名"(参数1, 参数2, ...)。这里有几个容易翻车的点。第一,DLL 文件名不带.dll后缀,写全了反而找不到。第二,参数可以是序列也可以是常数,通达信会自动把序列转成float*传进去,但常数会走pfParam,所以你的导出函数要同时处理这两种情况。第三,返回值如果是数值,直接就是指标线;如果返回字符串,需要用EXTERNSTR之类的函数接,具体看你的通达信版本支持情况。

我一般会先写一个“回显”函数,把公式传进来的参数原样打印出来,确认参数顺序和类型都对得上,再去写真正的业务逻辑。这个习惯帮我省过至少三次通宵排查。

// 回显参数,用于调试公式传参 __declspec(dllexport) int __stdcall TDX_ECHO( int nDataLen, float* pfIn, float* pfOut, int nParamCount, float* pfParam) { // 把第一个参数的值写到输出序列的每个位置 float v = (nParamCount > 0) ? pfParam[0] : -1.0f; for (int i = 0; i < nDataLen; i++) { pfOut[i] = v; } return 0; }

公式里写TDX_ECHO: "minimal_tdx@TDX_ECHO"(CLOSE, 123);,如果指标线是一条 123 的水平线,说明参数传递没问题。这个调试手段虽然土,但比看日志快得多。

3. 一机一码的落地:机器码采集、加密校验与授权文件

3.1 机器码从哪来,怎么保证换机就失效

一机一码的核心是“机器指纹”。在 Windows 上,常见做法是组合多个硬件标识:硬盘序列号、主板序列号、CPU 序列号、网卡 MAC 地址。单独用任何一个都不够稳——硬盘可以换,MAC 可以改,CPU 序列号在某些虚拟机上直接返回空。我一般会取硬盘序列号加主板序列号做拼接,再走一次哈希,得到一个固定长度的机器码。

采集硬盘序列号用DeviceIoControl配合SMART_GET_VERSION,主板序列号用 WMI 查Win32_BaseBoard。WMI 在部分精简系统上可能被阉割,所以要有降级方案:如果 WMI 拿不到,就退回到卷序列号。下面是一个采集硬盘序列号的片段:

// 获取物理硬盘序列号,返回 0 表示成功 #include <winioctl.h> int GetDiskSerial(char* pOut, int nOutLen) { HANDLE hDevice = CreateFileA( "\\\\.\\PhysicalDrive0", // 第一块物理硬盘 GENERIC_READ, FILE_SHARE_READ | FILE_SHARE_WRITE, NULL, OPEN_EXISTING, 0, NULL); if (hDevice == INVALID_HANDLE_VALUE) return -1; DWORD dwBytes = 0; STORAGE_PROPERTY_QUERY spq = {0}; spq.PropertyId = StorageDeviceProperty; spq.QueryType = PropertyStandardQuery; STORAGE_DESCRIPTOR_HEADER sdh = {0}; // 先拿描述头,确定需要多大缓冲区 if (!DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &spq, sizeof(spq), &sdh, sizeof(sdh), &dwBytes, NULL)) { CloseHandle(hDevice); return -1; } BYTE* pBuf = (BYTE*)malloc(sdh.Size); if (!DeviceIoControl(hDevice, IOCTL_STORAGE_QUERY_PROPERTY, &spq, sizeof(spq), pBuf, sdh.Size, &dwBytes, NULL)) { free(pBuf); CloseHandle(hDevice); return -1; } STORAGE_DEVICE_DESCRIPTOR* pSdd = (STORAGE_DEVICE_DESCRIPTOR*)pBuf; // SerialNumberOffset 是相对缓冲区起始的偏移 if (pSdd->SerialNumberOffset != 0) { char* pSerial = (char*)pBuf + pSdd->SerialNumberOffset; // 去掉尾部空格和换行 int n = 0; while (pSerial[n] && n < nOutLen - 1) { if (pSerial[n] != ' ' && pSerial[n] != '\r' && pSerial[n] != '\n') { pOut[n] = pSerial[n]; n++; } else break; } pOut[n] = '\0'; } free(pBuf); CloseHandle(hDevice); return 0; }

逻辑说明:先打开PhysicalDrive0,用IOCTL_STORAGE_QUERY_PROPERTY拿描述头,再根据返回的Size分配缓冲区拿完整描述,最后从SerialNumberOffset处读出序列号字符串。参数说明:pOut是输出缓冲区,nOutLen是缓冲区长度,调用方负责分配。注意这个函数在部分 NVMe 硬盘上可能返回空,所以实际项目里我会再叠一层卷序列号GetVolumeInformation作为兜底。

拿到机器码后,不要直接明文存授权文件。常见做法是:机器码 + 盐值 → SHA256 → 取前 16 字节作为授权码。DLL 启动时重新算一遍,和授权文件里的值比对。授权文件可以放在通达信目录下,也可以放注册表,但放注册表容易被清理软件干掉,我一般放一个隐藏属性的文件在T0002目录里。

3.2 授权校验写进 DLL 的哪个环节

校验不能放在公式每次调用时都做,那样性能扛不住,而且容易被抓调用栈。我一般会在 DLL 的DllMain里做一次初始化校验,把结果缓存在全局变量里,后续导出函数只读这个变量。如果校验失败,导出函数直接返回空序列或全零序列,让指标线消失,而不是弹窗报错——弹窗太容易被定位了。

// 全局授权状态,DllMain 里初始化一次 static int g_nAuthorized = 0; BOOL APIENTRY DllMain(HMODULE hModule, DWORD ulReason, LPVOID lpReserved) { if (ulReason == DLL_PROCESS_ATTACH) { // 采集机器码并校验授权文件 char szMachine[128] = {0}; if (GetDiskSerial(szMachine, sizeof(szMachine)) == 0) { char szExpected[64] = {0}; // 这里换成你自己的哈希逻辑 ComputeAuthCode(szMachine, szExpected); char szStored[64] = {0}; if (ReadAuthFile("T0002\\auth.dat", szStored, sizeof(szStored))) { // 常量时间比较,避免时序攻击(虽然在这个场景下有点过度设计) g_nAuthorized = (strcmp(szExpected, szStored) == 0) ? 1 : 0; } } } return TRUE; }

逻辑说明:DLL_PROCESS_ATTACH时做一次校验,结果存g_nAuthorized。导出函数开头判断这个标志,未授权就返回错误码或填充零值。参数说明:ReadAuthFile是自己实现的文件读取,路径用相对路径,避免暴露绝对路径。这里有个坑:通达信可能同时加载多个 DLL,如果你的授权文件被其他进程占用,读取会失败,所以打开文件时要用FILE_SHARE_READ。

授权文件的分发是另一个环节。用户拿到机器码后发给你,你用同样的哈希逻辑算出授权码,写进auth.dat发回去。这个过程可以做成一个小工具,但工具本身不要带采集逻辑,避免被逆向出你的盐值。盐值建议硬编码在 DLL 里,并且做一层简单的异或混淆,别直接写字符串常量。

4. 避坑与排查:DLL 加载失败、公式报错、授权误判

4.1 公式里调用 DLL 没反应,指标线空白

现象:公式保存成功,但指标线不出来,也没有任何报错。原因:九成是位数不匹配,64 位 DLL 被 32 位通达信加载,系统直接忽略。解决:用dumpbin /headers your.dll看 Machine 字段,必须是 x86。另一个可能是导出函数名被 C++ 编译器改名了,用extern "C"包住导出声明,或者直接看导出表确认名字。

4.2 编译通过但通达信提示“DLL 函数不存在”

现象:DLL 明明在目录里,公式却提示找不到函数。原因:通达信找 DLL 的路径和你放的位置不一致,或者 DLL 依赖了其他运行库(比如 MSVCR120.dll)而目标机器没装。解决:把 DLL 放到通达信主程序同级目录试一次,再用 Dependency Walker 看依赖是否齐全。我一般会静态链接运行库,避免这种环境问题。

4.3 机器码在部分机器上每次都不一样

现象:同一台电脑,重启后机器码变了,授权失效。原因:用了会变化的标识,比如动态磁盘卷序列号、可插拔网卡的 MAC。解决:优先用物理硬盘序列号加主板序列号,并且在采集失败时不要退回到随机值,而是返回一个固定错误码,让授权流程明确失败,而不是生成一个漂移的机器码。

4.4 授权文件被杀毒软件删除

现象:用户反馈昨天还能用,今天指标就没了。原因:授权文件被安全软件当成可疑文件清理了。解决:把授权文件写成非可执行格式,比如纯文本加自定义扩展名,并且放在通达信自己的目录下,减少被扫描的概率。同时 DLL 里要做容错,授权文件读不到时不要崩溃,只是返回未授权状态。

4.5 公式回测时 DLL 被反复加载导致卡顿

现象:回测大量股票时,通达信频繁加载卸载 DLL,速度明显变慢。原因:每次公式计算都可能触发 DLL 加载。解决:把耗时逻辑放在DLL_PROCESS_ATTACH里做一次,导出函数里只做轻量计算。另外避免在导出函数里做文件 IO 或网络请求,那些操作放在初始化阶段。

5. 进阶技巧:把授权校验做成可验证的闭环

5.1 用独立工具验证机器码与授权码的一致性

DLL 里的哈希逻辑一旦写错,排查起来很痛苦,因为通达信不会告诉你哪里错了。我的习惯是单独写一个控制台工具,复用同一份哈希代码,输入机器码输出授权码,和 DLL 里的结果比对。这样在发给用户之前,自己先跑一遍闭环。

// auth_tool.c 控制台验证工具 #include <stdio.h> #include <string.h> // 声明 DLL 里用到的哈希函数,实际项目里抽成公共头文件 extern void ComputeAuthCode(const char* pMachine, char* pOut); int main(int argc, char** argv) { if (argc < 2) { printf("usage: auth_tool <machine_code>\n"); return 1; } char szOut[64] = {0}; ComputeAuthCode(argv[1], szOut); printf("machine: %s\nauthcode: %s\n", argv[1], szOut); return 0; }

逻辑说明:这个工具和 DLL 链接同一份哈希实现,确保两边算出来的授权码完全一致。参数说明:argv[1]是机器码字符串,输出是授权码。实际使用时,让用户跑一个采集工具拿到机器码,你跑这个工具生成授权码,写进auth.dat发回去。

5.2 授权文件格式与版本号设计

授权文件不要只存一个哈希值,建议带上版本号和机器码摘要,方便后续升级授权逻辑时做兼容。格式可以用简单的键值对:

ver=1 machine=ABCD-EFGH-IJKL auth=9F86D081884C7D65

DLL 读取时先看ver,如果版本号高于自己支持的,就拒绝授权并提示用户升级 DLL。这样避免旧 DLL 读到新格式授权文件时产生误判。machine字段存机器码摘要而不是完整机器码,减少隐私暴露。

5.3 时间戳与离线授权的取舍

有些场景需要限时授权,比如试用七天。常见做法是在授权文件里加一个过期时间,DLL 里读系统时间比对。但系统时间可以被用户改,所以更稳的做法是记录首次运行时间,结合运行时长做累计。不过这会引入写文件操作,在通达信环境里要小心权限问题。我一般建议纯离线场景就用永久授权加机器绑定,限时需求走在线校验,但那就超出 DLL 编程的范围了。

5.4 一个我踩过的坑:哈希盐值硬编码被逆向

早期我把盐值直接写成字符串常量放在 DLL 里,结果被人用十六进制编辑器翻出来,做了个通用注册机。后来改成把盐值拆成多个片段,在运行时拼接,并且和机器码做异或后再哈希。虽然挡不住真正的高手,但至少提高了门槛。从那以后我每次做授权相关的 DLL,都会强制走一遍“盐值是否可被静态提取”的自检,确认没有明文常量才发布。

希望这些经验帮到你,少走几个我当年走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/20 4:54:34

Skills能力管理:从认知误区到四维拆解与验证方法

1. 从“skills”这个标题说起&#xff1a;一个被低估的元概念看到“skills”这个词&#xff0c;很多人第一反应是“技能”嘛&#xff0c;有什么好聊的。但如果你真的在职场、技术圈或者任何一个需要持续产出的领域待过几年&#xff0c;就会发现一个扎心的事实&#xff1a;绝大多…

作者头像 李华
网站建设 2026/10/20 4:54:23

32个uniapp项目源码拆包:从商城团购到工具类跨端素材库

简介&#xff1a;这份资源面向uniapp与mpvue方向的移动端开发者&#xff0c;尤其适合正在做小程序、跨端应用练手或需要参考成熟项目结构的中级学习者。包内共36个文件&#xff0c;以32个zip源码包和4个vue单文件组件为主&#xff0c;压缩包约97.04MB&#xff0c;zip多为可直接…

作者头像 李华
网站建设 2026/10/20 4:54:22

非洲草原动物目标检测实战:数据集构建与YOLO模型调优

1. 需求破题&#xff1a;五类动物、三个格式、一个可落地的识别方案本来这个项目的起点很简单&#xff1a;保护站的朋友想给草原上的常规动物做个自动识别&#xff0c;减少人工翻监控录像的时间。传统人工巡检五千多张图要数大半天&#xff0c;程序自动跑完一轮也就是喝杯咖啡的…

作者头像 李华
网站建设 2026/10/20 4:52:46

Java Socket实战:从BIO到NIO的高并发通信骨架与避坑指南

1. 这不是教科书里的Socket&#xff0c;是我在真实项目里反复调试出来的Java网络通信骨架“Java Socket编程基础及深入讲解”——看到这个标题&#xff0c;很多人第一反应是大学《计算机网络》课上那个用ServerSocket和Socket写个“Hello World”聊天程序的实验。但我要说&…

作者头像 李华
网站建设 2026/10/20 4:52:32

纯CSS实现3D旋转相册:零依赖、60fps、单HTML可分享

1. 项目概述&#xff1a;从一句气话到可落地的3D旋转相册实践“我对象说陪我过七夕&#xff0c;象说没时间&#xff0c;我一怒给女神做了一个某音上很火的3D旋转相册”——这句标题不是段子&#xff0c;而是真实发生在我认识的一位前端开发者身上的事。她当时刚结束连续三周的跨…

作者头像 李华