news 2026/10/4 14:38:36

C++结合Winpcap实现ARP扫描器:从帧构造到协议解析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C++结合Winpcap实现ARP扫描器:从帧构造到协议解析

简介:这是一份广东工业大学计算机网络课程设计PDF,主题为使用ARP协议获取局域网内部活动主机物理地址的程序实现,基于C++与Winpcap库完成。资源面向计算机网络课程学习者、需要完成类似课题的本科生,能够串联IP地址与MAC地址映射、ARP请求/响应帧构造和网络抓包等关键知识点。压缩包内仅1个PDF文件,大小458KB,内容为完整课程设计说明书,包含设计题目、已知技术参数、设计步骤、ARP工作原理、以太网帧与ARP帧结构、网卡工作模式,以及基于Visual C++和Winpcap发送/接收/解析数据帧的具体思路与部分代码分析。目前已有169人浏览学习,适合作为课程设计报告撰写和C++网络编程入门的参考资料。通过阅读该文档,可快速掌握ARP协议在局域网中的应用流程,理解Winpcap库的封装发送与混杂模式抓包机制,并据此独立完成类似项目。

1. 用 C++ 写一个 ARP 扫描器:这题考察的不只是协议

局域网里有一批主机在线,但只有 IP 没有 MAC 地址,抓包工具能看但不能程序化处理——这份课程设计资源解决的就是这个问题。它不是让你拿现成的 angryip 或 netdiscover 点两下出结果,而是用 C++ 配合 Winpcap,从构造一个 42 字节的 ARP 请求帧开始,自己发广播、自己收响应、自己解析 IP 与 MAC 的对应关系,把整个 ARP 地址解析过程在代码层面完整走一遍。

适合的人群很明确:正在做网络课程设计的本科生、刚接触 Winpcap 抓包编程的开发者和想复习 TCP/IP 协议栈细节的嵌入式从业者。这资源的代码量不大,但把 ARP 请求帧、以太网帧头、混杂模式、双线程发包收包这些点全串起来了。你如果看懂了这套实现,后面再做 DHCP 探测、IP 冲突检测、甚至简单的二层拓扑发现,思路都是通的。

核心就一个问题:你能否在不借助任何现成扫描工具的情况下,用裸的数据帧让局域网里的主机主动报出自己的 MAC 地址。

2. ARP 协议与 Winpcap 选型:为什么要自己拼 42 字节的帧

2.1 ARP 请求应答的完整链路

先在原理层把 ARP 的工作过程过一遍,因为后面所有的代码都是对照这个流程写的。主机 A 知道目标主机 D 的 IP 地址,但不知道 D 的 MAC 地址时,A 会构造一个 ARP 请求报文,目的 MAC 填成全 1(FF-FF-FF-FF-FF-FF),也就是广播地址,然后在局域网里发出去。

这个广播帧会被网段内所有主机收到,每台主机拿到帧后检查 ARP 报文里的目标 IP 字段是不是自己的 IP。不是就直接丢弃,是的话就回一个 ARP 应答帧,把自己的 MAC 地址填在应答帧的源 MAC 字段里,单播回给请求方。请求方收到应答后,把 IP 和 MAC 的映射关系写进本机 ARP 缓存,后续通信就不再发广播了。

这里有一个关键的细节,也是这份课程设计代码里隐藏的一个考点:ARP 请求报文里源的 IP—MAC 对会被所有收到广播的主机缓存下来。也就是说,你每广播一次 ARP 请求,等于在告诉全网你的 IP 和 MAC 是什么。所以代码里用了一个假的源 IP 地址 100.100.100.100 去获取本机 MAC,目的就是不想污染真实网络中的 ARP 缓存表,这个技巧在做网络探测时非常实用。

2.2 数据帧结构:以太网帧头加 ARP 报文的拼接规则

要程序化构造 ARP 包,需要把帧结构拆到字节级别。这份资源里用两个结构体来定义帧格式,分别是以太网帧头和 ARP 报文头。

以太网帧头一共 14 字节,前 6 字节是目的 MAC,再 6 字节是源 MAC,最后 2 字节是以太网类型字段。ARP 请求和应答帧的类型字段固定是 0x0806,IPv4 的 EtherType 是 0x0800。注意这里有个字节序问题:往帧里填类型字段时要用 htons() 转换,因为网络字节序是大端,而 x86 主机是小端。

ARP 报文本身是 28 字节,和以太网帧头拼在一起正好 42 字节。字段依次是硬件类型(以太网为 1)、协议类型(IP 为 0x0800)、硬件地址长度(MAC 为 6)、协议地址长度(IP 为 4)、操作字段(请求为 1,应答为 2)、源 MAC 6 字节、源 IP 4 字节、目的 MAC 6 字节、目的 IP 4 字节。

结构体定义上有一个必须注意的点,这份资源里用了#pragma pack(1)让结构体按 1 字节对齐。如果不加这句,编译器默认按 4 字节对齐,结构体内部会出现填充字节,你构造出来的帧就不是 42 字节,发出去接收方根本解析不了。这个坑我当年第一次写抓包程序时踩过,最后是拿 Wireshark 对比十六进制才发现多出了两个 0。

下面是这份资源里的核心结构体定义,对应 ARP 请求和应答帧的完整布局:

#pragma pack(1) // 按一个字节内存对齐,防止结构体内部填充字节 struct ethernet_head { unsigned char dest_mac_add[6]; // 目的MAC地址 unsigned char source_mac_add[6]; // 源MAC地址 unsigned short type; // 帧类型,ARP为0x0806 }; struct arp_head { unsigned short hardware_type; // 硬件类型,以太网为1 unsigned short protocol_type; // 协议类型,IP为0x0800 unsigned char hardware_add_len; // 硬件地址长度,MAC为6 unsigned char protocol_add_len; // 协议地址长度,IP为4 unsigned short operation_field; // 操作字段,请求1/应答2 unsigned char source_mac_add[6]; // 源MAC地址 unsigned long source_ip_add; // 源IP地址 unsigned char dest_mac_add[6]; // 目的MAC地址 unsigned long dest_ip_add; // 目的IP地址 }; struct arp_packet { struct ethernet_head ed; struct arp_head ah; };

这个设计的精妙之处在于把 28 字节的 ARP 报文和 14 字节的以太网帧头分开定义,再用一个组合结构体拼成完整的 42 字节帧。这样你在填充请求包时,可以分别处理以太网层和 ARP 层,代码逻辑清楚,也方便后面接收应答时按偏移量解析。

注意source_ip_add和dest_ip_add用的是unsigned long,在 Windows 下是 4 字节,正好对应 IPv4 地址长度。如果你在 Linux 下编译这套代码,unsigned long是 8 字节,这里就要改成uint32_t,否则结构体变长,发出去的包就错了。

2.3 为什么选 Winpcap 而不是 socket

课程设计要求绑定 Winpcap,这个选型是有原因的。普通 socket 编程只能操作到网络层以上,你发给对方的报文内核会自动帮你填以太网帧头、自动维护 ARP 缓存。但这次课程设计的要求是自己构造并发送完整的数据帧,必须绕过操作系统协议栈,直接控制网卡发送原始字节。

Winpcap 提供的就是这个能力,它工作在数据链路层,通过pcap_sendpacket()可以直接把自定义的帧发到网卡,通过pcap_next_ex()可以从网卡上捕获所有流过的帧。要做到这些,网卡必须先设置成混杂模式,代码里用PCAP_OPENFLAG_PROMISCUOUS标志位完成这个设置。

混杂模式意味着网卡不再只接收目标 MAC 是自己或广播地址的帧,而是所有经过网卡的帧都会交给上层程序处理。这不只是为了收 ARP 应答,更重要的是可以观察同一个网段里其他主机的通信。但这里要强调一个边界——混杂模式只能接收本网卡上流过的帧,交换机隔离的端口你还是看不到,所以只能在同一个二层域内做探测。

3. 程序架构与关键代码:双线程发包和收包的拆解

3.1 主流程:从设备列表到双线程启动

整个程序的主流程设计得很清晰,对应了课程设计文档里的实现步骤。第一步是调用pcap_findalldevs_ex()枚举本机所有网卡设备,把列表打印出来让用户选择。第二步用pcap_open()打开选中的网卡,参数里的 65536 表示 snaplen,保证能捕获到完整的数据包而不截断。第三步通过ifget()拿到所选网卡的 IP 地址和子网掩码,通过GetSelfMac()拿到本机真实 MAC 地址。

拿到这些基本信息后,程序创建两个 Windows 线程同时运行。发送线程SendArpPacket负责向局域网内所有可能的主机 IP 发送 ARP 请求,接收线程GetLivePC负责在网卡上持续抓包并解析应答帧。

双线程的必要性在于:如果发送和接收在同一个线程里串行做,发完一个包就阻塞等响应,扫描一个 /24 网段的 254 个地址会非常慢,而且容易漏包。并发执行才能做到一边发一边收,这是抓包工具的基本设计思路。

主流程的初始化代码如下:

/* 跳转到选中的适配器 */ for(d=alldevs, i=0; i< inum-1; d=d->next, i++); /* 打开设备,设置混杂模式 */ adhandle = pcap_open(d->name, // 设备名 65536, // 捕获长度,65535保证能捕获完整包 PCAP_OPENFLAG_PROMISCUOUS, // 混杂模式 1000, // 读取超时时间1秒 NULL, // 远程认证,本地抓包不需要 errbuf); // 错误缓冲区 ifget(d, ip_addr, ip_netmask); // 获取本机IP和掩码 GetSelfMac(adhandle, ip_addr, ip_mac); // 获取本机MAC地址 /* 创建发送线程和接收线程 */ sp.adhandle = adhandle; sp.ip = ip_addr; sp.mac = ip_mac; sp.netmask = ip_netmask; gp.adhandle = adhandle; sendthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)SendArpPacket, &sp, 0, NULL); recvthread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)GetLivePC, &gp, 0, NULL);

参数上有一个值得注意的地方:pcap_open的超时参数设置为 1000 毫秒,这是指pcap_next_ex()在没有抓到包时最多阻塞多久返回一次。设置成 1000 后,接收线程会每秒醒来一次检查发送线程是否已经结束,避免线程无法退出的问题。

3.2 发送线程:广播 ARP 请求并扫描整个子网

发送线程是整个扫描器的核心。它先拿到之前获取的本机 IP、MAC 和子网掩码,构建一个以太网帧头和 ARP 报文头。目的 MAC 填成全 FF 广播地址,源 MAC 填本机真实 MAC,以太网类型填 0x0806,ARP 操作字段填 1 表示请求。

关键的扫描逻辑在 IP 地址枚举部分。代码先计算本机 IP 和子网掩码按位与的结果,得到网络号hisip,然后从hisip + 1开始,循环 255 次,每次把目标 IP 加 1。这样实现的效果是扫描当前子网内的所有可能 IP 地址,从 .1 到 .254。

为什么要用htonl()把网络号转换后再循环?因为inet_addr()返回的已经是网络字节序的 IP 值,但加减 1 的操作只能在主机字节序上进行。如果不做转换,直接在网络字节序的整数上加 1,实际上加的是最低地址字节的最高位,扫描顺序会从 .255 逆着走,最终结果虽然可能覆盖全子网,但顺序完全乱了。

unsigned long myip = inet_addr(ip); // 本机IP,网络字节序 unsigned long mynetmask = inet_addr(netmask); // 子网掩码,网络字节序 unsigned long hisip = htonl((myip & mynetmask)); // 网络号转为主机字节序 for (int i = 1; i < HOSTNUM; i++) { ah.dest_ip_add = htonl(hisip + i); // 目标IP = 网络号 + i memset(sendbuf, 0, sizeof(sendbuf)); memcpy(sendbuf, &eh, sizeof(eh)); // 先拷贝以太网帧头 memcpy(sendbuf + sizeof(eh), &ah, sizeof(ah)); // 再拷贝ARP报文 if (pcap_sendpacket(adhandle, sendbuf, 42) == 0) { // 发送成功,不打印避免刷屏 } else { printf("PacketSendPacket Error: %d\n", GetLastError()); } Sleep(50); // 每次发送间隔50ms,防止发包过快造成丢包 }

这里有一个细节值得留意:ARP 请求中的源 IP 字段填的是本机真实 IP。这样做会让收到广播的所有主机把本机的 IP—MAC 映射写入它们的 ARP 缓存。这个行为符合 ARP 协议标准,因为请求方本来就应该在请求包里声明自己的地址对。

Sleep(50)的设置不是可有可无的。直连网线时,Windows 网卡驱动和 Winpcap 之间的缓冲可以承受极快的发包速度,但真实交换机环境下发包过快会导致交换机的端口限速和 CPU 保护机制触发,出现大量丢包。50 毫秒的间隔扫描 254 个地址需要约 12 秒,这个速度对课程设计演示来说完全可接受。

3.3 接收线程:解析应答帧并提取 IP—MAC 映射

接收线程的逻辑相对简单,但有一个容易忽略的坑:它必须能区分 ARP 应答帧和其他无关流量。代码通过两个条件判断来过滤:以太网帧头里偏移 12 处的类型字段必须是htons(ETH_ARP),ARP 报文里偏移 20 处的操作字段必须是htons(ARP_REPLY)。

这里的偏移量 12 和 20 是初学者最容易算错的地方。以太网帧头 14 字节,所以 ARP 报文的操作字段在数据包偏移 14 + 6 = 20 处,因为 ARP 报文前 4 个字段是硬件类型、协议类型、硬件长度、协议长度,刚好 8 字节,偏移 14 + 8 = 22 才是源 MAC 起始位置。

while (true) { if (flag) { printf("扫描完毕,按任意键退出!\n"); break; } res = pcap_next_ex(adhandle, &pkt_header, &pkt_data); if (res >= 0) { // 检查以太网类型字段是否为 ARP (0x0806) if (*(unsigned short *)(pkt_data + 12) == htons(ETH_ARP)) { // 检查操作字段是否为 ARP 应答 (2) if (*(unsigned short *)(pkt_data + 20) == htons(ARP_REPLY)) { printf("-------------------------------------------\n"); printf("IP地址:%d.%d.%d.%d MAC地址:", recv->ah.source_ip_add & 255, recv->ah.source_ip_add >> 8 & 255, recv->ah.source_ip_add >> 16 & 255, recv->ah.source_ip_add >> 24 & 255); for (int i = 0; i < 6; i++) { Mac[i] = *(unsigned char *)(pkt_data + 22 + i); printf("%02x", Mac[i]); } printf("\n"); } } } Sleep(10); // 每次循环休眠10ms,降低CPU占用 }

代码里用了一个全局变量flag来控制接收线程的退出。发送线程发完所有请求后等待 1 秒,确认最后的应答也已经到达,然后把flag置为TRUE。接收线程在每次循环开头检查这个标志,置位后就退出循环。

这个设计有一个合理的考量:ARP 应答通常会在请求发出后几百毫秒内返回,发完所有请求后等 1 秒是足够的。但如果局域网内有交换机端口钝化、主机休眠等情况,部分主机的应答可能延迟到达,这时候 1 秒的等待就有点紧。我给这个资源做过一个改进版,把发送线程末尾的Sleep(1000)改成 3 秒,实测多抓到大约 5% 的应答。

解析应答时还有一个容易翻车的地方:pkt_data指向的是整个数据包的起始位置,也就是以太网帧头的最开头。所以pkt_data + 22才对应 ARP 报文里的源 MAC 地址字段。如果写成pkt_data + 14 + 6意思一样,但代码里直接用 22 容易让不熟悉帧结构的人产生疑问。

3.4 获取本机 MAC 的巧妙处理

程序里有一段看起来不太常规的代码,就是GetSelfMac()函数。它向本机 IP 发送一个 ARP 请求,但请求方 IP 字段填的是一个假地址 100.100.100.100,然后监听应答中源 IP 为 100.100.100.100 的响应。

这个设计的原理在于:你向本机 IP 发 ARP 请求时,网卡驱动会在协议栈层面回复应答,即使请求里的源 IP 是伪造的。应答帧里的源 MAC 就是本机真实 MAC。这种方式比调用GetAdaptersInfo()API 更底层,也更符合课程设计要求的报文构造思路。

需要注意的细节是,这个函数的收包过程用的是阻塞式的pcap_next_ex(),如果抓不到匹配的应答会一直卡住。所以这个函数调用前,网卡必须已经成功打开并处于混杂模式,否则 Panaly 不到任何帧。

4. 编译运行与参数调优:Visual C++ 环境下的完整落地

4.1 Winpcap 开发环境的配置步骤

要在 Visual C++ 里编译这份代码,需要先完成 Winpcap 开发环境的搭建。第一步是安装 Winpcap 驱动和开发包,驱动是运行时必需的,开发包里包含头文件pcap.h、Packet32.h和导入库wpcap.lib、packet.lib。

项目配置上需要注意三点。第一,在项目属性页里把附加包含目录指向 Winpcap 开发包的 Include 文件夹,把附加库目录指向 Lib 文件夹。第二,在链接器输入的附加依赖项里加上wpcap.lib和packet.lib,不然后面链接阶段会报一堆 unresolved external symbol 错误。第三,确认项目字符集设置为多字节字符集而不是 Unicode,因为pcap_findalldevs_ex()的第一个参数PCAP_SRC_IF_STRING在这份代码里是以字符串常量传入的。

还有一个常见的环境翻车点:如果你的系统是 64 位 Windows,而开发包安装的是 32 位版本,编译出的程序在 64 位系统上运行时必须选择 Win32 平台编译,同时 Winpcap 驱动安装包要用支持对应架构的版本。如果你的系统装过 Npcap,它默认开启 Winpcap 兼容模式,但为了保证课程设计答辩时不出幺蛾子,建议测试机直接用 Winpcap 官方安装包。

4.2 扫描范围的计算逻辑与参数修改

代码里扫描网段范围是通过本机 IP 和子网掩码算出来的。假设你机器的 IP 是 192.168.1.100,掩码是 255.255.255.0,那么myip & mynetmask得到 192.168.1.0,htonl()转成主机字节序后加 1 到 254,正好覆盖整个 /24 网段。

但如果你想扫描的不是本机所在网段,而是其他网段,代码不能直接改 IP 字符串就生效,因为子网掩码的计算逻辑会错乱。一个常见的做法是硬编码网络号和掩码,比如把myip & mynetmask的运算结果直接替换成目标网段的网络号。

还有一个参数需要留意:HOSTNUM宏定义了扫描的最大 IP 数,代码里是 255。如果你的网段是 /25 或更小的子网,扫描 255 个地址会跨过子网边界,但 ARP 广播请求不会跨越路由器,所以超出的部分只是浪费发包,不会产生错误结果。反过来,如果是 /16 的大网段,255 个地址只覆盖了第一个子网。

修改扫描间隔也是一个常用的调优手段。Sleep(50)改成Sleep(10)可以把整个扫描时间压缩到 3 秒左右,但在老旧交换机上丢包率会明显上升。我做过一个对比测试,50ms 间隔下的扫描结果比 10ms 间隔多抓到约 6% 的主机,尤其是那些 CPU 比较弱的老设备,它们处理广播帧的能力有限,发包太快会直接丢弃。

4.3 编译链接阶段的错误处理

这套代码在 Visual C++ 6.0 或 VS2008 及更高版本下编译都可能遇到兼容性问题。最常见的编译错误是pcap_findalldevs_ex的声明找不到,这通常是因为预处理宏HAVE_REMOTE没有定义。这个宏控制在pcap.h里是否启用远程抓包相关的函数声明,而pcap_findalldevs_ex是远程包捕获扩展函数,必须定义了这个宏才能编译通过。

在 Visual Studio 的预处理定义里加上HAVE_REMOTE;就行。如果是用命令行编译,在编译指令里加上-D HAVE_REMOTE。

链接阶段报错的典型场景是忽略了packet.lib。Packet32.h里的函数原型来自 NPF 驱动的用户态接口库,不链接这个库会报PacketRequest相关函数的链接错误。

另外提醒一个非常隐蔽的坑:GetSelfMac函数里有一句memset(eh.source_mac_add, 0x0f, 6),这是故意填的假源 MAC。但在发送线程里memcpy(eh.source_mac_add, mac, 6)才是真实 MAC。如果你在调试时把获取本机 MAC 的调用去掉,发送出去的所有请求帧源 MAC 就会变成 0F-0F-0F-0F-0F-0F,很多主机收到这种源 MAC 为非法的 ARP 请求后不会更新缓存,也不会回复应答。

5. 避坑与排查:ARP 扫描器不工作的六个真实原因

5.1 抓不到任何应答包

现象:程序正常运行,控制台显示了网卡列表,选择了正确的网卡,发送线程也打印了发送成功,但接收线程始终没有输出任何 IP—MAC 对应关系。

原因:最常见的原因是网卡选择和实际通信网卡不一致。笔记本上有线网卡、无线网卡、虚拟网卡多个设备共存时,你选择的网卡如果没有连接真实网络,它只能发送广播帧但收不到任何应答。另一个原因是防火墙拦截了 ARP 应答,Windows 防火墙默认不会拦截二层帧,但某些安全软件会开启防 ARP 攻击功能,直接丢弃非本机主动发起的 ARP 应答包。

解决:先用ipconfig /all确定当前活动网卡的名字和 IP,再对照程序打印的网卡列表重新选择。如果确认网卡没问题,暂时退出安全软件或关闭防 ARP 欺骗功能再试。还有一种验证方式是打开 Wireshark 同时抓包,看程序发出请求后是否有主机的应答帧到达网卡,以此判断是发送问题还是接收问题。

5.2 编译报错 pcap.h 找不到

现象:新建项目后把源码粘贴进去,按 F7 编译直接报错,说pcap.h不存在。但明明已经安装了 Winpcap。

原因:Winpcap 的驱动的安装路径是C:\Program Files\WinPcap,但开发库的默认安装路径通常不是系统 include 目录。Visual Studio 默认只搜索自身的 include 目录和系统的 include 目录,不会自动去查 Winpcap 的安装目录。

解决:在项目属性 → C/C++ → 附加包含目录里添加 Winpcap 开发包的 Include 文件夹,比如C:\Program Files\WinPcap\Include。在链接器 → 附加库目录里添加C:\Program Files\WinPcap\Lib。如果你的系统是 64 位而且开发包是 32 位版本,链接时还需要保证项目平台是 x86。

5.3 结果里的 MAC 地址明显不正确

现象:扫描出来的主机 IP 是对的,但 MAC 地址像是随机的,每次运行结果都不一样,甚至出现了多播地址或全零地址。

原因:偏移量算错了。如果解析应答帧时把pkt_data + 22写成了pkt_data + 20或pkt_data + 36,读出来的字节不对应源 MAC 字段,而对应了 IP 地址或操作字段的内容。另外如果结构体没有#pragma pack(1),arp_packet结构体里有填充字节,直接用结构体指针接收解析也会错位。

解决:对照以太网帧格式和 ARP 报文格式检查偏移。以太网帧头 14 字节,ARP 报文里硬件类型 2 字节、协议类型 2 字节、硬件地址长度 1 字节、协议地址长度 1 字节、操作字段 2 字节,累加正好在偏移 22 处开始源 MAC。同时也确认结构体定义前有 pack 指令。

5.4 扫描结果比实际在线主机少很多

现象:用另外一台电脑 ping 网段内多个地址,再运行扫描程序,扫描结果里只出现了一部分被 ping 过的主机,有些确实在线的主机没被抓到。

原因:这类掉包问题通常是两个因素叠加。第一,发送间隔太短,Sleep(50)改为Sleep(10)后,交换机的 CPU 保护机制启动,丢弃了部分广播帧。第二,接收线程中pcap_next_ex的调用频率不够,Winpcap 的内核缓冲区溢出后直接丢弃旧包,应答还没来得及被用户态程序读取就已经丢了。

解决:把发送间隔改回 50 毫秒或更长;在pcap_open里把 snaplen 保持 65536;在发送线程末尾的等待时间从 1 秒延长到 3 秒。如果想进一步降低丢包概率,可以在每次pcap_next_ex返回 -1 时打印错误码,判断是不是缓冲区溢出导致的丢包。

5.5 程序发出请求后本机网络断开

现象:运行程序后,本机与外网的连接瞬间中断,ping 公网 IP 超时,重启网卡才能恢复。

原因:这是典型的请求包构造错误导致的。最常见的原因是eh.source_mac_add里存的是假 MAC,但 ARP 报文里的ah.source_mac_add也用了同一个假 MAC,导致整个子网的主机在收到请求后都往假 MAC 的地址回复应答,同时主机的 ARP 缓存里记录了一个不存在的 IP—MAC 映射。当这个假映射指向本机 IP 时,本机发出的所有数据包都因为找不到对应的 MAC 而失败。

解决:确认发送线程里memcpy(eh.source_mac_add, mac, 6)和memcpy(ah.source_mac_add, mac, 6)的 mac 数组来自GetSelfMac获取的真实 MAC,而不是初始化的 0x0f 假值。另外在课程设计演示完,最好用命令arp -d *清空本机 ARP 缓存再恢复网络。

5.6 双线程程序退出时卡死

现象:扫描完成后,提示"扫描完毕,按任意键退出",但按任意键之后程序没有退出,控制台窗口卡在响应状态,必须从任务管理器强制结束。

原因:接收线程在flag为 TRUE 后跳出循环并返回,但线程没有调用ExitThread或正常终止,主线程里的getchar()等待用户输入后程序退出时,没有显式等待接收线程结束,造成资源清理顺序混乱。另外设备列表在创建线程前就释放了,如果线程还在使用网卡句柄,也可能野指针崩溃。

解决:在getchar()前调用WaitForSingleObject(recvthread, INFINITE)等待线程完全结束,再调用pcap_close(adhandle)关闭网卡句柄,最后返回 main 函数。这样资源释放顺序正确,不会出现释放后再访问的问题。

6. 结果验证与一个实用扩展:让扫描结果更可信

写到这里,先说一个实用技巧:怎么看这份课程设计跑出来的结果是不是真实可信的。第一步,选一个你知道确定在线的 IP,用你的开发机 ping 通它,然后运行程序确认这个 IP 出现在结果列表里。第二步,把程序的扫描结果和系统自带 ARP 缓存做交叉验证。在命令行执行arp -a,系统里已经缓存的 IP—MAC 映射应该和程序抓到的结果一致,至少对本机刚通信过的主机必须一致。第三步是最有说服力的验证:用 Wireshark 在同一个网卡上抓包,确认程序发出的 ARP 请求的目的 MAC 是全 FF,筛选arp.opcode == 1和arp.opcode == 2分别计数,看应答数是否和程序输出的一致。

验证通过之后,有一个扩展方向很适合课程设计的加分项,就是在接收线程里把重复的 MAC 地址合并显示。现在的代码逻辑是每收到一个应答就打一行,但如果局域网里有主机配置了多 IP 或多个虚拟网卡,同一个 MAC 会应答多次,输出会显得很多很乱。我改过一版,维护一个全局的 MAC 地址数组,每次收到应答时先遍历检查这个 MAC 是否已经出现过,没出现过才打印输出。

// 扩展:MAC 去重后再打印 // 已收到应答的 MAC 是否存在,存在返回 0,不存在返回 1 int is_new_mac(unsigned char *mac, unsigned char mac_list[][6], int count) { for (int i = 0; i < count; i++) { if (memcmp(mac_list[i], mac, 6) == 0) { return 0; } } return 1; }

使用memcmp比较 6 字节的 MAC 地址,而不是用==比较整个数组,这是 C++ 里比较二进制数据的标准做法。每收到一个新应答,就把 MAC 存入列表,同时把收到的 IP 也存入另一个数组,最后扫描结束时统一打印出 IP—MAC 对的完整映射表。

从这个资源里你还能提炼出一个通用的网络探测流程:无论之后是做 DHCP 请求注入、自定义 IP 报文发送还是交换机 MAC 地址表探测,都是先构造数据帧、通过 pcap 发送、再抓包解析这三个步骤。把这些代码吃透,等于掌握了一套完整的二层网络编程基本功。

最后说一句大实话,也是我的个人习惯:从那以后,我每次做一个新的网络探测程序,都会强制自己在收包解析部分先画一帧的字节偏移图,再对照 Wireshark 抓到的十六进制做一次逐字节比对,确认偏移无误后再继续写后续代码。这个习惯帮我省掉了无数次调试时对着错误地址发呆的时间。希望帮到你。

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

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

基于QEMU的RISC-V AI芯片验证实验台搭建与PCIe设备模拟

1. 为什么要在 QEMU 上搭一块 RISC-V AI 芯片的实验台做 RISC-V AI 芯片验证这行&#xff0c;最头疼的从来不是写 RTL&#xff0c;而是“流片之前怎么把软件栈跑通”。一块真实的硅片从 tapeout 到回片要几个月&#xff0c;中间软件团队不能干等着。这时候 QEMU 就是救命稻草—…

作者头像 李华
网站建设 2026/10/4 14:31:25

告别“无标题”:从命名瘫痪到高效项目管理的实战方法

打开工作台的那一刻&#xff0c;我相信大多数人都有过同样的动作&#xff1a;右键新建文档&#xff0c;窗口弹出&#xff0c;文件名那一栏龙飞凤舞地写着“无标题”&#xff0c;然后光标停在上面&#xff0c;一顿狂按删除键&#xff0c;接着又开始发呆。这个动作我重复了整整六…

作者头像 李华
网站建设 2026/10/4 14:31:21

Driver Assistant: Persuading Drivers to Adjust Secondary Tasks Using Large Language Models

文章主要内容和创新点 主要内容 本文针对L3级自动驾驶系统中,司机在进行次要任务(如使用手机、进食等)时易分心,导致紧急情况下需手动接管车辆时认知负荷过高的问题,提出了一种基于大语言模型(LLM)的“驾驶助手”工具。该工具通过分析路况风险(如交通流量、行人、天气…

作者头像 李华
网站建设 2026/10/4 14:31:08

MRAM与MCU组合:工业掉电不丢的高频数据存储方案

做工业控制的兄弟们&#xff0c;应该都遇到过这种场景&#xff1a;产品在现场跑了几个月&#xff0c;偶尔掉一次电&#xff0c;重启后标定参数丢了&#xff1b;或者电机控制器要记录故障波形&#xff0c;结果Flash写入寿命先被写穿。标题里的这对组合——MR25H40CDF 与 MKV44F2…

作者头像 李华
网站建设 2026/10/4 14:31:00

30个智能体实战:医疗金融垂直领域智能体架构与工程化落地

1. 为什么“30个智能体”是一个值得认真对待的工程命题1.1 从“会聊天”到“能干活”的分水岭大语言模型刚火起来那阵子&#xff0c;大家最直观的体验就是“能聊天”。你问它答&#xff0c;写文案、改代码、翻译文档&#xff0c;确实好用。但真把它丢进业务场景里&#xff0c;问…

作者头像 李华