news 2026/10/24 6:47:33

VC++驱动源程序:DriverEntry、IOCTL通信与加载排错

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
VC++驱动源程序:DriverEntry、IOCTL通信与加载排错

简介:面向Windows内核驱动开发初学者与进阶者的VC++驱动源程序合集,围绕设备驱动框架、IRP处理与内核调试等核心问题,提供可参考、可复用的工程素材。压缩包共657个文件,约7.73MB,以304个.h头文件、149个.lib库文件、34个.c与12个.cpp源码为主体,并配有.dsp工程、.inf安装信息、.sys驱动模块及.rc资源脚本,便于按模块梳理工程结构与编译依赖。已有155人学习下载。内容涉及DriverEntry初始化、设备对象与驱动接口、中断服务例程、同步互斥、内存与硬件资源分配,以及WDF框架、代码签名和WinDbg调试思路,适合用来对照理解Windows内核机制、搭建实验环境并开展二次开发。

1. VC++ 驱动源程序:一个压缩包解开的到底是些什么

去年帮人收拾一台老工控机,厂商的采集卡驱动早就不更新了,对方只丢过来一个压缩包,解压开全是 .c、.h、.inf、.vcxproj,文件名里清一色带着 drv 后缀。这不是普通 VC++ 工程,而是跑在 Windows 内核里的 VC++ 驱动源程序——用 C 写入口,靠 WDK 编译成 .sys,再由系统加载进 Ring0。它适合两类人:一类是手里有老硬件、老设备,原厂驱动没了但协议还清楚,得自己接上去;另一类是刚接触内核开发,想拿一份能编译、能加载、能跟用户态通信的最小骨架当模板。下面按这个包实际的拆法,从工程骨架一路读到能改出自己 IOCTL 的程度。

2. 工程骨架:WDK 驱动工程和普通 C++ 工程差在哪

2.1 vcxproj 里那几行决定生死的配置

把一个 .vcxproj 拖进编辑器,第一眼看过去和普通控制台工程差不多,但真正决定它是不是驱动的,是ConfigurationType和DriverType这两个节点。普通 exe 是Application,驱动必须是Driver,再配TargetVersion指定目标系统版本,WDK 才会把内核库和 CRTL 换成内核版的运行库,把入口点从main换成DriverEntry。这一步配错了,编出来的文件后缀能是 .sys,但一加载就报“映像格式不正确”,纯属玄学。

<!-- 驱动工程的关键属性,缺一项都会让链接阶段跑偏 --> <PropertyGroup> <ConfigurationType>Driver</ConfigurationType> <!-- 不是 Application --> <DriverType>KMDF</DriverType> <!-- 内核模式驱动,也可写 WDM --> <TargetVersion>Windows10</TargetVersion> <!-- 决定链接哪套内核库 --> <UseDebugLibraries>false</UseDebugLibraries> <PlatformToolset>v143</PlatformToolset> </PropertyGroup> <PropertyGroup> <!-- 内核里没有用户态异常处理,这两个必须关 --> <ExceptionHandling>false</ExceptionHandling> <BufferSecurityCheck>false</BufferSecurityCheck> </PropertyGroup>

ConfigurationType写成Driver只是告诉构建系统“我要的是驱动”,DriverType才决定用 KMDF 还是裸 WDM 框架。这个源程序包用的是 WDM 风格,也就是自己IoCreateDevice、自己挂MajorFunction,没有 KMDF 那层对象封装。好处是链路透明,每一层都能看见;坏处是所有清理工作得自己兜。BufferSecurityCheck关掉是因为 /GS 的 cookie 依赖用户态运行时,内核态链接时会找不到符号。常见做法是把TargetVersion和测试机的系统版本对齐,跨版本加载虽然多数时候能成,但DriverEntry里的版本判断会给你添麻烦。

我一般拿到一份陌生驱动工程,先不改代码,只做一件事:把配置从 Debug 切到 Release 各编一遍,看两边产物大小差多少。差得离谱就说明有断言或者调试输出没被条件编译包住,这类代码一旦进正式环境,DbgPrint 刷屏能把整机拖慢。

2.2 目录结构逐层看:哪一层是业务,哪一层是胶水

这个包的目录不算深,但分层挺清楚,顺着看能很快判断出哪块该动、哪块别碰。

MyDrv/ ├── inc/ │ └── MyDrvIoctl.h # 用户态和内核态共用的 IOCTL 码,必须两边一致 ├── src/ │ ├── DriverEntry.c # 入口:创建设备、符号链接、挂分发函数 │ ├── Dispatch.c # IRP 分发:CREATE/CLOSE/DEVICE_CONTROL │ ├── Hardware.c # 真正的业务:端口读写、寄存器操作 │ └── DriverUnload.c # 卸载:删符号链接、删设备对象 ├── inf/ │ └── MyDrv.inf # 安装脚本,写服务项和硬件 ID ├── user/ │ └── MyDrvTest/ # 用户态测试程序,CreateFile + DeviceIoControl └── MyDrv.sln

inc目录里那个头文件是整个工程里最值得先读的文件,它定义了双方约定的控制码,任何一端改了而另一端没跟着改,通信就断。src/Hardware.c是业务层,和内核 API 耦合最少,二次开发时改动量最大也最安全。user/MyDrvTest是配套的验证工具,很多人下载驱动包只编驱动不编它,结果加载成功也不知道通没通,白白多排查半小时。

inf目录容易被忽略。WDM 驱动有两种加载方式:一种是sc create手工建服务,适合调试;另一种是走 inf 安装,适合分发。inf 里的ServiceBinary路径、StartType、HardwareID三项写不对,设备管理器里就是一个黄色感叹号,而且报的错常常和真实原因无关。

2.3 编译产物:.sys 之外还得盯住两个文件

Release 编完,输出目录里除了.sys,还有.pdb和.cer。.pdb是符号文件,调试时没有它,蓝屏的调用栈就是一堆地址,等于看天书。.cer是签名流程里生成的证书,测试阶段用它给 .sys 签名,测试机上导入这张证书之后才能加载。

编译命令可以直接用 msbuild 驱动解决方案,省得开 IDE:

:: 先确认 WDK 环境变量已就位,再编 Release x64 msbuild MyDrv.sln /p:Configuration=Release /p:Platform=x64 /t:Rebuild

/t:Rebuild而不是/t:Build,是因为驱动工程里有过中间文件残留导致链接到旧 .obj 的情况,血泪经验是每次改完头文件都强制重建。/p:Platform=x64别省,默认可能落到 Win32,编出来的东西在 64 位系统上根本加载不了。

3. DriverEntry 到设备对象:内核里第一段必须跑通的代码

3.1 DriverEntry 里四件必须做对的事

DriverEntry是驱动的main,系统加载 .sys 后第一个调的就是它。它的返回值决定加载成败,返回非STATUS_SUCCESS系统会立刻卸载映像,事件日志里只留一句很含糊的话。这段代码要按顺序做四件事:创建设备对象、设置缓冲方式、创建符号链接、挂分发函数。

#include <ntddk.h> #include "MyDrvIoctl.h" #define DEVICE_NAME L"\\Device\\MyDrvDevice" #define SYMLINK_NAME L"\\DosDevices\\MyDrvDevice" // 入口:系统加载映像后第一个调用,返回值直接决定加载成败 NTSTATUS DriverEntry(PDRIVER_OBJECT pDriverObj, PUNICODE_STRING pRegistryPath) { NTSTATUS status = STATUS_SUCCESS; PDEVICE_OBJECT pDevObj = NULL; UNICODE_STRING devName, symName; UNREFERENCED_PARAMETER(pRegistryPath); // 1. 创建设备对象,名字必须是 \Device\ 下的完整路径 RtlInitUnicodeString(&devName, DEVICE_NAME); status = IoCreateDevice(pDriverObj, 0, &devName, FILE_DEVICE_UNKNOWN, // 设备类型,未知设备最通用 FILE_DEVICE_SECURE_OPEN, // 做安全检查 FALSE, // 非独占 &pDevObj); if (!NT_SUCCESS(status)) { return status; // 设备建不出来,后面全是空谈 } // 2. 指定缓冲方式,三选一,必须和用户态调用方式匹配 pDevObj->Flags |= DO_BUFFERED_IO; // 3. 建符号链接,用户态才能用 \\.\MyDrvDevice 打开 RtlInitUnicodeString(&symName, SYMLINK_NAME); status = IoCreateSymbolicLink(&symName, &devName); if (!NT_SUCCESS(status)) { IoDeleteDevice(pDevObj); // 失败要回滚,不然设备泄漏 return status; } // 4. 挂分发函数,没挂的 IRP 会返回 STATUS_INVALID_DEVICE_REQUEST pDriverObj->MajorFunction[IRP_MJ_CREATE] = DispatchCreateClose; pDriverObj->MajorFunction[IRP_MJ_CLOSE] = DispatchCreateClose; pDriverObj->MajorFunction[IRP_MJ_DEVICE_CONTROL] = DispatchDeviceControl; pDriverObj->DriverUnload = DriverUnload; pDevObj->Flags &= ~DO_DEVICE_INITIALIZING; // 最后一步才允许外部打开 return STATUS_SUCCESS; }

四个参数的坑集中在IoCreateDevice上。第三个参数是设备名,必须是\Device\开头的完整路径;第二个参数是设备扩展大小,这个包传 0,意味着不存额外状态,如果后面要保存端口基址之类的信息,这里得改成sizeof(DEVICE_EXTENSION),读取时用pDevObj->DeviceExtension。最后一个DO_DEVICE_INITIALIZING标志,必须在所有初始化做完之后再清,提前清掉会让外部在你还没准备好时就打开设备,蓝屏的概率不低。

DO_BUFFERED_IO这一行决定了后面 IOCTL 怎么写。三种缓冲方式只能选一种,选DO_BUFFERED_IO就用pIrp->AssociatedIrp.SystemBuffer读写,系统帮你拷贝;选DO_DIRECT_IO就用 MDL 锁页;不设则走METHOD_NEITHER,用户态传进来的裸地址需要自己校验,稍不注意就被利用。

3.2 符号链接与卸载路径:不对偶就是内存泄漏

符号链接是内核设备名和用户态可见名之间的桥。用户态CreateFile(L"\\\\.\\MyDrvDevice")实际被重定向到\Device\MyDrvDevice。名字对不上就是ERROR_FILE_NOT_FOUND,很多人第一反应是驱动没加载,其实sc query一看服务是 RUNNING,只是符号链接拼错了一个字符。

卸载函数必须和DriverEntry严格对偶,建了什么就删什么,顺序还得反过来:

// 卸载:顺序必须和 DriverEntry 相反,先删链接再删设备 VOID DriverUnload(PDRIVER_OBJECT pDriverObj) { UNICODE_STRING symName; RtlInitUnicodeString(&symName, SYMLINK_NAME); IoDeleteSymbolicLink(&symName); // 先断掉用户态入口 IoDeleteDevice(pDriverObj->DeviceObject); // 再销毁设备对象 }

先删链接再删设备是有讲究的。反过来的话,在两步之间有一个时间窗,用户态还能通过残留的符号链接去打开一个已经销毁的设备,直接蓝屏。这段代码短,但漏掉任意一行,sc stop之后就再也sc start不起来了,因为符号链接还占着名字,第二次IoCreateSymbolicLink返回STATUS_OBJECT_NAME_COLLISION。

这个包在卸载里还多做了一步:把DeviceExtension里申请的非分页内存和端口资源一并释放。凡是MmAllocateNonCachedMemory、IoAllocateMdl、HalTranslateBusAddress拿到的资源,都必须在DriverUnload里还回去。内核没有垃圾回收,漏一次就永久缺一块内存,重启才能恢复。

4. IRP 分发与 IOCTL 通信:用户态和内核态怎么对上话

4.1 从 IRP 栈里取参数:三个长度必须分清

用户态调DeviceIoControl之后,I/O 管理器把它翻译成一个IRP_MJ_DEVICE_CONTROL的 IRP,交给驱动挂的分发函数。分发函数里第一件事永远是通过IoGetCurrentIrpStackLocation拿到本次请求的参数块,控制码、输入长度、输出长度全在里面。这三个字段混用的后果是缓冲区越界,而内核里的越界不是崩溃,是随机改坏别人的内存,症状千奇百怪。

NTSTATUS DispatchDeviceControl(PDEVICE_OBJECT pDevObj, PIRP pIrp) { PIO_STACK_LOCATION stack; ULONG code, inLen, outLen, info = 0; NTSTATUS status = STATUS_INVALID_DEVICE_REQUEST; PVOID buf; UNREFERENCED_PARAMETER(pDevObj); stack = IoGetCurrentIrpStackLocation(pIrp); code = stack->Parameters.DeviceIoControl.IoControlCode; inLen = stack->Parameters.DeviceIoControl.InputBufferLength; outLen = stack->Parameters.DeviceIoControl.OutputBufferLength; buf = pIrp->AssociatedIrp.SystemBuffer; // BUFFERED_IO 下就是这一块 switch (code) { case MY_IOCTL_GET_VERSION: // 输出缓冲区必须够大,否则返回 STATUS_BUFFER_TOO_SMALL 而不是硬写 if (outLen >= sizeof(ULONG)) { *(ULONG *)buf = 0x00010000; // 高 16 位主版本,低 16 位次版本 info = sizeof(ULONG); // 告诉调用方实际写了多少 status = STATUS_SUCCESS; } else { status = STATUS_BUFFER_TOO_SMALL; } break; case MY_IOCTL_READ_PORT: // 输入长度校验永远放在访问 buf 之前 if (inLen < sizeof(PORT_REQUEST)) { status = STATUS_INVALID_PARAMETER; break; } status = DoReadPort((PPORT_REQUEST)buf, outLen, &info); break; default: break; } pIrp->IoStatus.Status = status; pIrp->IoStatus.Information = info; // 不设这个,用户态拿到的返回长度是 0 IoCompleteRequest(pIrp, IO_NO_INCREMENT); return status; }

InputBufferLength和OutputBufferLength是分别由用户态的nInBufferSize、nOutBufferSize决定的,两者都指向同一块SystemBuffer。缓冲区复用意味着写输出之前必须先确认输入已经读完,否则会把输入参数覆盖掉。Information字段返回给用户态的第 7 个参数,忘记赋值的时候,用户态DeviceIoControl返回成功但lpBytesReturned是 0,很容易被误判成失败。

IoCompleteRequest的第二个参数是优先级提升值,绝大多数同步分发用IO_NO_INCREMENT就够。如果驱动内部把 IRP 挂起异步处理,那分发函数必须先返回STATUS_PENDING,再在别的线程里完成它,这两步顺序搞反就是经典的“完成一个已经完成的 IRP”,蓝屏代码0x8E。

4.2 IOCTL 控制码:宏里的四个参数各管什么

控制码用CTL_CODE拼,四个参数分别是设备类型、功能号、传输方式和访问权限。这个宏生成的 32 位值会被 I/O 管理器解析,设备类型和访问权限对不上,请求根本到不了你的分发函数。

// inc/MyDrvIoctl.h,内核态和用户态都要包含这个头文件 #ifndef MYDRV_IOCTL_H #define MYDRV_IOCTL_H #define MY_DEVICE_TYPE 0x8000 // 自定义设备类型,取 0x8000 以上避免和系统冲突 // 功能号从 0x800 开始,0x000-0x7FF 留给系统保留 #define MY_IOCTL_GET_VERSION CTL_CODE(MY_DEVICE_TYPE, 0x800, METHOD_BUFFERED, FILE_ANY_ACCESS) #define MY_IOCTL_READ_PORT CTL_CODE(MY_DEVICE_TYPE, 0x801, METHOD_BUFFERED, FILE_READ_ACCESS | FILE_WRITE_ACCESS) #endif

第二个参数是功能号,0x800 以下是预留给系统的,自己用必须从 0x800 起。第三个参数METHOD_BUFFERED必须和DriverEntry里设的DO_BUFFERED_IO一致,不一致的时候 I/O 管理器会按控制码里的方式去处理缓冲区,而你的代码按另一种方式访问,等于自己给自己埋雷。第四个参数是访问权限,FILE_ANY_ACCESS最宽松,FILE_READ_ACCESS之类的会让用户态在没带对应GENERIC_READ权限打开设备时直接被拒,错误码是ERROR_ACCESS_DENIED,看起来像权限问题,其实是控制码定义问题。

头文件必须两边共用同一份,不能各写各的。我见过一次翻车就是用户态工程里复制了一份控制码,结果功能号抄错一位,驱动端一直走default分支返回STATUS_INVALID_DEVICE_REQUEST,排查了两小时才发现是复制粘贴的锅。

4.3 用户态配套代码:一个最小验证程序

驱动加载成功不代表能用,必须有一个用户态程序把链路打通。这个包里user/MyDrvTest就是干这个的,核心只有三步:打开设备、发控制码、关句柄。

#include <windows.h> #include <cstdio> #include "../../inc/MyDrvIoctl.h" int main() { // 用户态看到的路径是 \\.\ + 符号链接名,注意转义 HANDLE hDev = CreateFileW(L"\\\\.\\MyDrvDevice", GENERIC_READ | GENERIC_WRITE, 0, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hDev == INVALID_HANDLE_VALUE) { printf("open failed, err=%lu\n", GetLastError()); return 1; } ULONG version = 0, returned = 0; BOOL ok = DeviceIoControl(hDev, MY_IOCTL_GET_VERSION, NULL, 0, // 无输入 &version, sizeof(version),// 输出缓冲区 &returned, NULL); if (!ok) { printf("ioctl failed, err=%lu\n", GetLastError()); } else { printf("version=%u.%u returned=%lu\n", (version >> 16) & 0xFFFF, version & 0xFFFF, returned); } CloseHandle(hDev); return 0; }

CreateFileW的路径里\\.\是固定写法,四个反斜杠在 C 字符串里转义成两个,写成一个或三个都打不开。dwShareMode传 0 表示独占,如果驱动里IoCreateDevice的Exclusive参数传了 TRUE,这里也必须传 0,两边不一致会返回ERROR_SHARING_VIOLATION。最后一个参数是 OVERLAPPED 指针,同步调用传 NULL 即可,传了就得处理异步完成。

DeviceIoControl的返回值和GetLastError()要分开看。返回 FALSE 时错误码才有意义;返回 TRUE 但returned是 0,说明驱动没设IoStatus.Information,这是驱动侧的问题不是调用侧的问题。这个区分能省掉大量无谓的排查。

5. 编译签名与加载排错:五个真踩过的坑

5.1 从 .sys 到能跑起来,中间还差签名和安装

编出 .sys 只完成了一半,现代 Windows 对内核模块有签名强制。测试阶段最省事的路径是打开测试签名模式,再给自己的 .sys 签一张测试证书。

:: 测试机上执行,开启测试签名模式后必须重启 bcdedit /set testsigning on :: 生成自签证书并入发布者存储(管理员权限) makecert -r -pe -ss PrivateCertStore -n "CN=MyDrvTest" MyDrvTest.cer certmgr -add MyDrvTest.cer -s -r localMachine root certmgr -add MyDrvTest.cer -s -r localMachine trustedpublisher :: 给驱动签名 signtool sign /v /s PrivateCertStore /n MyDrvTest /t http://timestamp.example.com MyDrv.sys

时间戳服务器参数在实际环境里要换成本机可达的地址,内网机器访问不了外网时间源的时候signtool会卡住不回。签名完成之后用signtool verify /v /kp MyDrv.sys自查,输出里Kernel Mode Signing那一段必须是通过状态。

加载走手工建服务最快,调试期不用折腾 inf:

:: 注意 type= 和 binPath= 后面那个空格,少一个就报语法错误 sc create MyDrv type= kernel binPath= C:\drv\MyDrv.sys sc start MyDrv sc query MyDrv sc stop MyDrv sc delete MyDrv

这套命令里sc create的等号后面必须跟空格,写成type=kernel会直接返回“参数错误”。binPath用绝对路径,相对路径在内核加载时是相对于System32\drivers解析的,放错位置的结果是ERROR_FILE_NOT_FOUND,但事件日志常常不写具体路径。

5.2 五个高频问题:现象、原因、解决

现象一:sc start返回错误 577,事件日志写“数字签名无法验证”。原因是 .sys 没签名,或者签了但证书没进trustedpublisher。测试签名模式下系统只信任已导入的证书链,光开关打开不够。解决办法是重新执行certmgr -add把 .cer 导进本地计算机的“受信任的发布者”,再signtool sign重签一遍。还有一个隐蔽情况:改过代码之后忘了重签,旧签名和新文件哈希对不上,验证一样失败。

现象二:加载成功,但用户态CreateFile一直返回ERROR_FILE_NOT_FOUND。原因是符号链接没建成功,或者名字拼错。IoCreateSymbolicLink的第二个参数是目标设备名,第一个是用户态可见名,两个写反了就会出现“服务在跑但打不开”。排查方法是进 WinObj 之类工具看\Device\下有没有你的设备名、\DosDevices\下有没有对应链接。写反的情况在第一次写驱动时特别常见。

现象三:调用DeviceIoControl直接蓝屏,代码0x0000000A或0x0000001E。原因是分发函数里访问了没校验的指针或者越界缓冲区。METHOD_NEITHER方式下SystemBuffer是 NULL,直接解引用必炸;METHOD_BUFFERED方式下如果用户态传的nOutBufferSize小于驱动写入的字节数,SystemBuffer后面就是别人的内存。解决办法是每个分支进来先判长度再碰指针,所有输出前都对比outLen,宁可返回STATUS_BUFFER_TOO_SMALL也别硬写。写完拿驱动验证工具跑一遍,能提前抓出大半。

现象四:sc stop之后再也sc start不起来,返回STATUS_OBJECT_NAME_COLLISION。原因是DriverUnload里符号链接没删干净,或者第一次加载失败时回滚路径漏了删设备。名字被占住之后,第二次IoCreateSymbolicLink必然冲突。解决分两步:先在 WinObj 里确认残留对象,重启清掉;再把DriverEntry里所有失败分支都补上IoDeleteDevice和IoDeleteSymbolicLink,形成完整的失败回滚链。

现象五:Debug 版能跑,Release 版一加载就崩。原因是代码里有依赖调试运行时的操作,或者断言里藏了副作用。内核态 Release 会关掉 /GS 和部分安全检查,ASSERT在 Release 下整个被编掉,写在里面的赋值语句跟着消失,逻辑就断了。解决办法是把所有带副作用的表达式从ASSERT里挪出来,单独成行;再用DbgPrint加条件编译包住的日志,逐段确认走到哪一步。

6. 二次开发:加一个自己的 IOCTL 到底改哪几处

拿到这份源程序,最常做的改造是加一个业务控制码,比如读某个寄存器或者下发一段配置。改动点固定在四个位置,漏一个链路就断。第一处在inc/MyDrvIoctl.h里加控制码定义,功能号在已有最大值上递增,传输方式沿用METHOD_BUFFERED,别混着来;第二处在DispatchDeviceControl的 switch 里加分支,进来先校验inLen/outLen,再调业务函数;第三处在src/Hardware.c里实现真正的读写逻辑;第四处在用户态测试程序里照着已有调用复制一份,换成新的控制码。

改动位置改什么漏改的后果
inc/MyDrvIoctl.h新增 CTL_CODE 定义两端控制码不一致,走 default 分支
Dispatch.cswitch 新分支 + 长度校验请求被忽略,返回 STATUS_INVALID_DEVICE_REQUEST
Hardware.c实际业务实现返回成功但没有任何效果
user/MyDrvTest调用侧代码无法验证,出问题定位不到哪一端

业务函数本身建议保持“只碰缓冲区、不碰 IRP”的形态,把 IRP 相关的东西全留在分发层。这样改的时候业务代码可以单独拷出来测,不用每次都加载驱动。我一般写成NTSTATUS DoXxx(PVOID buf, ULONG inLen, ULONG outLen, PULONG pInfo),分发层只负责判长度和转调,出错返回什么状态码由业务层决定,边界清楚之后调试成本能降一大截。

验证顺序也有讲究:先单编驱动确认无警告,再加载看sc query是不是 RUNNING,然后用测试程序发已知控制码确认老功能没被改坏,最后才发新控制码。这个顺序反过来的话,新功能不通时你会怀疑是驱动没加载、签名失效还是自己逻辑错,三种可能混在一起排查,时间全耗在试错上。改完记得重签名,测试签名模式下旧证书对应的哈希变了同样加载失败。

从那以后我每次动这份源程序,都强制走一遍“改头文件→改分发→改业务→改测试→重建→重签→按顺序验证”的固定路径,一步都不跳。驱动这东西出错代价太大,一次蓝屏可能就是一上午的活白干。希望帮到你。

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

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

Win7 64位离线安装.NET Framework 4.0全攻略:从选型到验证

简介&#xff1a;这份资源是面向Windows 7 64位系统用户的.NET Framework 4.0独立安装包&#xff0c;主要解决64位环境下部分.NET应用程序因缺少运行时而无法启动、依赖组件缺失等问题&#xff0c;适合需要部署或维护.NET桌面程序的开发者与普通用户使用。压缩包共2个文件&…

作者头像 李华
网站建设 2026/10/24 6:44:51

3天从85%降到20%!这3个降AI率工具让我查重秒变原创

为什么你的论文总被判定AIGC超标&#xff1f; 很多同学以为用自己的话复述AI生成的内容就万事大吉&#xff0c;但现在的检测系统远比我们想象的聪明。通过这次经历&#xff0c;我总结出三个最常见的原因&#xff1a;首先是文本模式化&#xff0c;AI生成的段落往往具有特定的词汇…

作者头像 李华
网站建设 2026/10/24 6:43:13

面向具身操作的视觉-语言-动作模型综述:启元Q1的“大脑”进化之路

摘要&#xff1a;视觉-语言-动作&#xff08;VLA&#xff09;模型是机器人从“听懂指令”走向“物理执行”的核心大脑。本文以《自动化学报》2026年发表的《面向具身操作的视觉-语言-动作模型综述》为核心蓝本&#xff0c;系统解析了VLA模型的发展历程、架构拆解、数据金字塔、…

作者头像 李华
网站建设 2026/10/24 6:41:52

2026年江西安全员A证考试题库+答案+解析

2026年安全员ABC证考试精准题库⬆️⬆️⬆️ 点击上方卡片立即获取⬆️⬆️⬆️ 考点练习题型练习易错题模拟考错题集收藏夹A 证考什么&#xff1f;先花 1 分钟搞清楚A 证面向企业主要负责人——法定代表人、总经理、分管安全生产的副总经理这一类人。考的不是现场怎么搭架子&a…

作者头像 李华