ARTICLE DETAIL

资讯详情

深耕网站SEO优化与搜索引擎排名提升的一线实战洞察。

在 Windows 上用 MinGW 搭建 lwIP 调试环境:方法与踩坑实录

在 Windows 上用 MinGW 搭建 lwIP 调试环境:方法与踩坑实录 目标在 Windows 上把 lwIP 协议栈跑起来作为学习 TCP/IP 和嵌入式网络开发的模拟环境。本文只讲搭建 排坑TCP 通信验证和 Wireshark 抓包分析留到下一篇。全文分三部分准备环境清单、路线选择、项目结构→编译配置构建方法与两个编译坑→网络配置lwipcfg.h最终配置一览 四个网络坑的原理与调试方法。第一部分准备1.1 为什么在 Windows 上跑 lwIPlwIP 是为嵌入式系统设计的轻量级 TCP/IP 协议栈核心代码src/是平台无关的纯 C。它通过移植层抽象出线程、定时器、网卡收发等能力——这意味着同一份协议栈代码既能烧进单片机也能跑在 Windows 上。在 Windows 上跑起来的整体结构是应用程序 (example_app) ↓ lwIP 协议栈核心 (src/core, 和嵌入式设备上是同一份代码) ↓ win32 移植层 (contrib/ports/win32: sys_arch.c 映射 Win32 线程, pcapif.c 收发帧) ↓ Npcap (绕过 Windows 自己的协议栈, 直接读写网卡上的原始以太网帧) ↓ 物理网卡 ↔ 路由器/局域网关键点lwIP 通过 Npcap绕过 Windows 自己的 TCP/IP 栈用自己的 MAC 和 IP 独立收发。对路由器来说这就是接在同一根线上的另一台设备。所以在 Windows 上学到的协议行为DHCP 状态机、TCP 握手、重传可以直接迁移到真实嵌入式项目。1.2 环境清单目录位置仅供参考项目版本/位置说明OSWindows 11 (10.0.26200) x64编译器MSYS2 UCRT64gcc 16.1.0C:\msys64\ucrt64\bin\gcc.exe构建工具cmake 4.4.0ninja同在 MSYS2 中lwIP 源码2.2.2 (git 仓库)含src/contrib/Npcap 运行时已安装C:\Windows\System32\wpcap.dllNpcap SDK1.16,D:\npcap-sdk-1.16含Include/pcap.h、Lib/x64/wpcap.lib各组件来源lwIP 源码git指令git clone https://github.com/lwip-tcpip/lwip.gitGitHub 仓库是官方镜像主仓库托管在 savannah.nongnu.org两边内容同步MSYS2https://www.msys2.org 下载安装包。装完后在 UCRT64 终端直接在搜索栏搜索MSYS2 UCRT64,别用错了用 pacman 安装工具链pacman -S mingw-w64-ucrt-x86_64-gcc mingw-w64-ucrt-x86_64-cmake mingw-w64-ucrt-x86_64-ninjaNpcap installer SDK都在 https://npcap.com 的下载页——运行时选Npcap installer装 Wireshark 时一般会带上SDK 是单独的压缩包免费下载许可允许个人学习用途解压出来就是上表里的npcap-sdk-1.16目录Wireshark观察协议时用https://www.wireshark.org 安装过程中会提示安装 Npcap最容易搞混的一点Npcap 运行时 ≠ Npcap SDK。运行时是让程序能用网卡抓包的驱动装 Wireshark 时一般会带上SDK 是编译用的头文件和.lib需要单独从 https://npcap.com 下载。两个都要。老旧教程避坑:很多老教程使用的 WinPcap已停止维护。虽然可从其官网下载WpdPack开发包例如4.1.2版本。 但是这里不建议使用毕竟现在wireshark会主动帮你安装新版Npcap使用WinPcap可能造成错误。关于终端建议用 MSYS2 安装时自带的UCRT64 终端gcc/cmake/ninja 安装后开箱可用但要保证 环境变量PATH 里有C:\msys64\ucrt64\bin。1.3 路线选择MinGW 还是 MSVClwIP 的 win32 移植提供两套构建入口contrib/ports/win32/msvc/下的 Visual Studio 工程和 CMake 文件。我的机器没装老版本的 Visual Studio这个我跟着博客试过因为版本不兼容问题难以成功配置但有 MSYS2 的 gcc所以选了MinGW gcc CMake Ninja路线——不用装几个 GB 的 VS也不用特意找到老版本立刻能走通。本文使用这条路线。1.4 开工前项目结构和三层配置体系先花两分钟看清 lwIP 仓库的结构:lwip/ ├── src/ # 协议栈核心, 平台无关 (烧进单片机的就是这份代码) │ ├── core/ # TCP/UDP/IP/ICMP/DHCP 等协议实现 │ ├── api/ # socket / netconn API │ └── include/ # 头文件, 含 lwip/opt.h ├── contrib/ # 移植层和示例 (不进单片机, 只在 PC 模拟时用) │ ├── ports/win32/ # Windows 移植: 线程适配 Npcap 网卡驱动 │ ├── examples/example_app/ # 示例应用和它的配置 │ └── apps/ # 附加应用 (ping, socket_examples 等) └── test/ doc/ # 测试和文档, 本篇不涉及三层配置体系是本篇所有改配置动作的总地图层级文件作用改不改默认值层src/include/lwip/opt.h定义全部编译期选项的默认值协议开关、内存池大小、超时……永远不改要覆盖去下一层协议栈配置层contrib/examples/example_app/lwipopts.h覆盖 opt.h 的默认值比如打开DHCP_DEBUG调试输出3.4 节打开 DHCP 日志改的就是这里应用配置层contrib/examples/example_app/lwipcfg.hexample_app 的私有配置选哪块网卡、MAC、静态 IP、DHCP 开关、跑哪些示例应用本篇改得最多的文件需要根据个人情况进行配置关于lwipcfg.h值得单独说一句它不是 lwIP 核心的一部分只是 example_app 这个示例的配置从同目录的lwipcfg.h.example复制而来注意文件名里没有 opts。真实嵌入式项目里你会写自己的这一层。它管的都是这次模拟要怎么跑网卡序号/GUID、MAC 和 IP、USE_DHCP/USE_AUTOIP开关、LWIP_PING_APP等应用开关——这些改动全部集中在 3.1 节讲。本篇动过的文件清单全部改动控制在 4 个文件内文件属于哪层改了什么章节lwipcfg.h应用配置网卡 GUID、MAC、静态 IP、关 DHCP/AutoIP、开 ping 应用3.1lwipopts.h协议栈配置打开DHCP_DEBUG排查用3.4contrib/ports/win32/Filelists.cmakewin32 移植构建脚本Npcap SDK 按 SYSTEM 包含、精确压制告警2.3src/include/lwip/errno.h核心代码唯一动的#undef补丁解决 errno 重定义2.2另有两个文件没改但值得知道位置contrib/ports/win32/include/arch/cc.h是编译器适配层2.2 节排查 errno 冲突时的关键现场contrib/ports/win32/pcapif.c是基于 Npcap 的网卡驱动3.1 节MAC 最后一字节 1的规则就出自它。第二部分编译配置2.1 构建的正确姿势兼驳网上教程的两个错误正确流程在 lwIP 仓库根目录下# 复制示例配置(一次性)这一步一定提前做否则cmake无法通过 cp contrib/examples/example_app/lwipcfg.h.example \ contrib/examples/example_app/lwipcfg.h mkdir build cd build cmake .. -G Ninja -DWPDPACK_DIRD:/npcap-sdk-1.16 cmake --build .成功后生成build/contrib/ports/win32/example_app/example_app.exe。错误教程 1直接对 example_app 目录配置有些博客让你这样cd lwip/contrib/ports/win32/example_app mkdir build cd build cmake .. -G MinGW Makefiles ...对 lwIP 2.2.x 不适用。打开contrib/ports/win32/example_app/CMakeLists.txt第一行就是include(${LWIP_DIR}/contrib/ports/CMakeCommon.cmake)——它没有project()LWIP_DIR要靠顶层CMakeLists.txt通过add_subdirectory()传入。直接对它配置必败。这类博客多半针对旧版本或改过的工程。错误教程 2用 CMAKE_PREFIX_PATH 指定 pcap SDKcmake .. -DCMAKE_PREFIX_PATHC:/npcap-sdk ...传了也白传。看contrib/ports/win32/Filelists.cmakeWindows 分支下 wpcap 库路径是硬编码的${WPDPACK_DIR}/lib/x64/wpcap.lib只认-DWPDPACK_DIR从不查询CMAKE_PREFIX_PATH。教训博客/AI/文档给的方案必须对着本地这份源码验证。源码永远是最终事实。2.2 编译坑 1errno 重定义EWOULDBLOCK / ETIMEDOUT现象第一批编译错误十几个文件报同一个错src/include/lwip/errno.h:88:10: error: EWOULDBLOCK redefined [-Werror] 88 | #define EWOULDBLOCK EAGAIN /* Operation would block */ C:/msys64/ucrt64/include/pthread_compat.h:108:9: note: this is the location of the previous definition 108 | #define EWOULDBLOCK 140排查上面现象里贴的 error 行和 note 行其实是同一条报错的两个部分这是当时src/core/init.c的完整报错原文In file included from src/include/lwip/sockets.h:54, from src/core/init.c:47: src/include/lwip/errno.h:88:10: error: EWOULDBLOCK redefined [-Werror] 88 | #define EWOULDBLOCK EAGAIN /* Operation would block */ In file included from C:/msys64/ucrt64/include/pthread_time.h:27, from C:/msys64/ucrt64/include/time.h:329, from C:/msys64/ucrt64/include/sys/time.h:10, from contrib/ports/win32/include/arch/cc.h:55, from src/include/lwip/arch.h:48, from src/include/lwip/debug.h:40, from src/include/lwip/opt.h:52, from src/core/init.c:38: C:/msys64/ucrt64/include/pthread_compat.h:108:9: note: this is the location of the previous definition 108 | #define EWOULDBLOCK 140每条链自下而上读就是谁拉进了谁。如果报错里没带链比如 Ninja 并行输出被截断还有个专门的工具gcc -H会把每个源文件的 include 树按缩进打出来冲突双方分别在树的哪个分支一目了然。整理后的对峙图init.c:47 → sockets.h → lwip/errno.h:88 (EWOULDBLOCKEAGAIN11) init.c:38 → opt.h → debug.h → arch.h → cc.h:55 → sys/time.h → ... → pthread_compat.h (EWOULDBLOCK140)contrib/ports/win32/include/arch/cc.h里非 MSVC 编译器一律#define LWIP_PROVIDE_ERRNO为老编译器兜底的历史假设于是 lwIP 定义自己的 errno而 MinGW 的sys/time.h又拉进了pthread_compat.h两边定义值还不一样-Werror下直接失败。弯路改用系统 errno第一反应是让 MinGW 走系统 errnocc.h 改定义LWIP_ERRNO_STDINCLUDE。编译确实过了但后来发现这是埋雷lwIP 的应用层代码假设EWOULDBLOCK EAGAINLinux 下二者都是 11比如contrib/apps/socket_examples/socket_examples.c里就有LWIP_ASSERT(errno EAGAIN, ...)而 UCRT64 里EAGAIN11、EWOULDBLOCK140不相等——用到 socket API 时断言就会炸。正解保留 lwIP 自带 errno用 #undef 消冲突lwIP 自带定义里EWOULDBLOCK EAGAIN和上游假设一致。不能用它的唯一原因是和系统头重定义那就先#undef再定义。在src/include/lwip/errno.h打两行补丁#ifdef EWOULDBLOCK /* some C libraries (e.g. MinGW-w64/UCRT64) define EWOULDBLOCK ! EAGAIN; * undefine it so that lwIPs own definition ( EAGAIN) takes effect */ #undef EWOULDBLOCK #endif #define EWOULDBLOCK EAGAIN /* Operation would block */注意这段补丁必须放在#define EWOULDBLOCK EAGAIN之前ETIMEDOUT同样处理。冲突的两个宏之外其余 errno 值双方一致互不影响。这个补丁改动最小、语义最正确。(这里也有教程采用的是改变包含顺序但是这里个人感觉直接用#undef更加安全)2.3 编译坑 2Npcap SDK 头文件触发 -Werror现象D:/npcap-sdk-1.16/include/npcap-bpf.h:128:22: error: C style comments are incompatible with C90 D:/npcap-sdk-1.16/include/npcap-defs.h:131:1: error: redundant redeclaration of __C_ASSERT__ contrib/ports/win32/pcapif_helper.c:43:7: error: hFile is deprecatedlwIP 的 CMake 给自己的代码配了极严格的告警-Wall -pedantic -Werror -Wc90-c99-compat ...第三方 Npcap 头文件根本扛不住。解法自己的代码和第三方代码用不同标准对待修改contrib/ports/win32/Filelists.cmake原来是add_library(lwipcontribportwindows EXCLUDE_FROM_ALL ${lwipcontribportwindows_SRCS}) target_include_directories(lwipcontribportwindows PRIVATE ${LWIP_INCLUDE_DIRS} ${WPDPACK_DIR}/include ${LWIP_MBEDTLS_INCLUDE_DIRS}) target_compile_options(lwipcontribportwindows PRIVATE ${LWIP_COMPILER_FLAGS})改为add_library(lwipcontribportwindows EXCLUDE_FROM_ALL ${lwipcontribportwindows_SRCS}) target_include_directories(lwipcontribportwindows PRIVATE ${LWIP_INCLUDE_DIRS} ${LWIP_MBEDTLS_INCLUDE_DIRS}) # 第三方 SDK 头文件按 SYSTEM 包含: 不适用 lwIP 的严格告警 target_include_directories(lwipcontribportwindows SYSTEM PRIVATE ${WPDPACK_DIR}/include) target_compile_options(lwipcontribportwindows PRIVATE ${LWIP_COMPILER_FLAGS} # port 代码访问了 SDK 标记为 deprecated 的内部成员, 只压这一个告警 $$C_COMPILER_ID:GNU:-Wno-deprecated-declarations)两个原则SYSTEM 包含目录告诉编译器这是第三方头别用我的标准要求它。SDK 头文件不归我们管不可能去改它压制告警要精确到点只关-Wno-deprecated-declarations这一个而不是-w或删-Werror——大面积关告警是掩盖问题lwIP 自己的代码继续受严格约束有些博客教你注释掉-Wc90-c99-compat、全局加-Wno-unused-parameter那是把lwIP对自己代码的纪律也一起废了不建议。第三部分网络配置3.1 lwipcfg.h 最终配置一览网络部分的配置全在contrib/examples/example_app/lwipcfg.h一个文件里。先把最终能跑通的完整配置集中给出来——方便各位参照配置每个值为什么这么填、怎么查出自己机器上的值见后续各小节的排坑实录。/* lwipcfg.h 网络相关最终配置值要换成你自己机器的 */ /* 1. 网卡选择序号 GUID 双保险为什么 → 3.2 节 */ #define PACKET_LIB_ADAPTER_NR 5 #define PACKET_LIB_ADAPTER_GUID D83D00A3-B69D-4368-ABC0-9B1AAB9F4E61 /* 2. MAC用有线网卡的真实 MAC。注意 pcapif.c 会把基准 MAC 最后一字节 * 加上 netif-num多网卡防冲突my_mac_addr[5] netif-num * 我的真实 MAC 末字节是 8F、网卡 num1所以基准要填 8E1 后落回 8F */ #define LWIP_MAC_ADDR_BASE {0x38,0xA7,0x46,0x0F,0x97,0x8E} /* 3. 静态独立 IP关掉 DHCP/AutoIP为什么 → 3.4、3.5 节 */ #define USE_DHCP 0 #define USE_AUTOIP 0 #define LWIP_PORT_INIT_IPADDR(addr) IP4_ADDR((addr), 192,168,0,200) #define LWIP_PORT_INIT_GW(addr) IP4_ADDR((addr), 192,168,0,1) #define LWIP_PORT_INIT_NETMASK(addr) IP4_ADDR((addr), 255,255,255,0) /* 4. 打开自带的 ping 应用验证用目标默认是网关 */ #define LWIP_PING_APP 1三个查询/确认动作GUID 和真实 MACPowerShell 里Get-NetAdapter | Select-Object Name, InterfaceGuid, MacAddress, Status静态 IP选路由器网段里空闲的地址改之前先 ping 一下确认没人用序号直接运行一次example_app.exe它会枚举本机网卡列表下节就讲这个列表怎么读注意每次改完一定重新cmake --build .再运行!!!。下面是配置过程中建立的四个应有认识以及对应的调试方法。3.2 应有认识 1网卡选择——序号会漂移GUID 才可靠编译通过后直接运行example_app.exe它会先枚举本机网卡像是0: NPF_{9198ECB8-...} Desc: WAN Miniport (Network Monitor) 1: NPF_{47D94BE7-...} Desc: WAN Miniport (IPv6) 2: NPF_{5696E8A8-...} Desc: WAN Miniport (IP) 3: NPF_{A63E4EA8-...} Desc: Bluetooth Device (Personal Area Network) 4: NPF_{054CEC85-...} Desc: Killer(R) Wi-Fi 6E AX1675i ... 5: NPF_{D83D00A3-...} Desc: Killer E3100G 2.5 Gigabit Ethernet Controller 6-7: ... Desc: Microsoft Wi-Fi Direct Virtual Adapter ... 8: NPF_Loopback Desc: Adapter for loopback traffic capture这个列表怎么读序号就是lwipcfg.h里PACKET_LIB_ADAPTER_NR要填的值。0-2 是 Windows 的 VPN/PPPoE 虚拟协议驱动3 是蓝牙6-7 是 Wi-Fi Direct 虚拟适配器——都不能用。只有 4物理无线和 5物理有线是真实网卡。默认配置选的是序号 1WAN Miniport一块废卡所以起来后 IP 永远是 0.0.0.0。你自己的未必和我相同仅供参考别只填序号要用 GUID 双保险序号会因为增删设备而漂移——我插上网线后有线网卡从序号 8 变成了 5。GUID 是 Windows 给每块网卡的唯一 ID查法见 3.1 节。程序启动时按 GUID 找到网卡还会自动纠正序号打印出Using adapter_num: 5也就是说其实这里你也不必一定配置正确数字——序号漂移从此无感。3.3 应有认识 2假 MAC 行不通——根源在网卡把哪些帧交上来一句话总结要用真实 MAC本节要用静态独立 IP3.5 节!!lwipcfg.h默认给 lwIP 配一个编造的 MACLWIP_MAC_ADDR_BASE {0x00,0x01,...}。它能不能用取决于收包路径上的每个环节是否愿意把目的 MAC ≠ 自己的帧投递给 lwIP有线看网卡。支持混杂模式的网卡会把所有帧交上来假 MAC 通常没事但我这块 Killer E3100G 只投递发给自己 MAC 的单播帧——实测假 MAC 下 DHCP 同样拿不到地址3.4 节的 169.254其实就是有线 假 MAC的产物Wi-Fi理论上必败。802.11 要求帧的源 MAC 必须是已关联到 AP 的站点 MAC即网卡真实 MACNpcap 在 Wi-Fi 上做伪以太网转换会把发出帧的源 MAC 改写成真实 MAC而 lwIP 自认的是假 MAC回包比对不认丢弃AP 侧按假 MAC 回包也因未关联而丢所以无论有线还是无线MAC 都要填网卡真实值这就是 3.1 配置里第 2 项的来历。至于 Wi-Fi我把真实 MAC 配置上后实测了一次依然不通发得出、收不回障碍在 Npcap/驱动更底层。建议直接用有线网线别在 Wi-Fi 上纠缠。3.4 应有认识 3169.254.x.x 不是 DHCP 成功换有线后此时跑的还是假 MAC这正是下面 DHCP 收不到回应的根因见 3.3等了很久输出终于出现了第三行status_callbackUP, local interface IP is 169.254.7.4我以为我成功了但……别高兴太早——169.254.x.x 是 AutoIP 链路本地地址意思是 DHCP 一直没拿到租约lwIP 的 AutoIP 兜底机制自己编了一个。真正的 DHCP 成功应该是路由器网段的地址如 192.168.0.x。分段定位法打开 DHCP_DEBUG为了解决这个问题我们可以用分段定位法在contrib/examples/example_app/lwipopts.h里把DHCP_DEBUG后面改成LWIP_DBG_ON重编译运行日志一目了然dhcp_discover() dhcp_discover: sendto(DISCOVER, IP_ADDR_BROADCAST, LWIP_IANA_PORT_DHCP_SERVER) dhcp_discover(): set request timeout 2000 msecs dhcp_fine_tmr(): request timeout dhcp_discover() ... (2s → 4s → 8s → 16s → 60s 指数退避重试, 全程没有一条 dhcp_recv)判读逻辑协议调试的通用套路有 discover 但永远没 recv → 发得出去、收不回来问题在收包路径或对方不理睬如果连 discover 都没有 → lwIP 内部问题。当然如果你遇到了其他问题可以把对应的选项改成ON来进行观察3.5 应有认识 4同机 ping 不通是原理性限制排查过程中自然想从 Windows ping 一下 lwIP 试试结果永远不通。折腾一圈后确认这不是 bug是以太网的工作原理。交换机的基本规则——帧不会被发回它的入端口hairpin 过滤。Windows 发出谁是 192.168.0.200的 ARP 广播交换机泛洪到除了这个网口之外的所有端口同一块网卡上的 lwIP 收不到就算 lwIP 回应回应帧的目的 MAC 就在入端口上交换机照样过滤。同一台机器上的两个协议栈无法通过物理链路互相通信。注意两个报错文案的区别信息量很大无法访问目标主机ARP 解析失败链路层就不通请求超时ARP 成功了但 ICMP 没回应问题在更高层正解配置层面按 3.1 节来就行——真实 MAC 加静态独立 IP、关掉 DHCP/AutoIP避免 lwIP 和 Windows 同 MAC 拿到同 IP 互相干扰。验证层面记住一条要用第三方设备lwIP 自己 ping 网关或局域网里另一台机器 ping lwIP别用本机。3.6 跑通结果日志status_callbackUP, local interface IP is 192.168.0.200 ping: send 192.168.0.1 ping: recv 0:0:0:0:0:ffff:c0a8:1 16 msping: recv后面那个怪地址是 IPv4-mapped IPv6 格式的显示c0a8:1c0.a8.00.01 192.168.0.1启用了 IPv6 的构建里lwIP 内部统一用双格式ip_addr_t打印走了 IPv6 路径纯显示问题。附git 管理教训真实踩坑代价半天搭建过程中我在 git 上栽了两跤值得记下来冲突解决后没编译就提交解决冲突时残留了一个孤字符/在#endif行尾提交后编译直接报/* within comment。铁律冲突解决完先编译再提交。格式化提交静默回退了逻辑一个标题写着调整目标地址为 IPv4 映射地址的提交这个我让ai帮我起的名实际 diff 却把这个调整改回了原值还顺手做了 100 多行空格调整。提交信息与实际内容完全相反。铁律git commit前必须git diff --staged过一遍。事后清理用的是git reset --hard 最后一个健康提交——因为坏提交只在本地没推送如果已经 push 共享了要用git revert新增反向提交而不是改写历史。总结与预告至此lwIP 已经在 Windows 上作为一台独立设备跑起来了有自己的 IP192.168.0.200能和路由器双向通信。全程改动控制在 4 个文件内其中只有lwip/errno.h两行涉及核心代码。回顾最有价值的三个认知报错归类 顺 include 链找冲突双方比逐条看错误快得多自己的代码和第三方代码用不同告警标准SYSTEM 包含压制告警精确到点网络不通先分层定位DHCP 日志 → ARP → ICMP并警惕测试方法本身不成立同机 ping
返回列表