应用场景
需要在windows平台编译lwip源码进行源码仿真调试,做一些协议细节方面验证工作。由于Visual Studio 开发环境过大,打算利用vscode + cmake + MinGW-64搭建基础环境。
软件版本
软件 | 版本 |
---|---|
lwip | 2.2.1 |
cmake | 3.22.1 |
MinGW-W64 | 8.1.0 |
WpdPack | 4.1.2 |
Visual Studio2022 | 17.12.3 |
lwip最新源码从github下载。
遇到的问题
在 VSCode 使用 MinGW-W64 编译lwip源码过程中部分库函数 [undefined reference to `tftp_init_client’]
使用微软的Visual Studio MSVC 编译器链接却没有问题,如下图。
原因分析
1、首先明确两种开发编译环境的异同点。
- 使用相同的cmake脚本。
- 细节处理方面稍有不同。
- 调用的编译工具链不同。
- 但是源文件与库的引用方式一致。
2、检查cmake相关生成规则的定义。
apps库cmake生成规则如下。
# cmake脚本位置 xxx\lwip\src\Filelists.cmake
# TFTP server files
.....
set(lwiptftp_SRCS
${LWIP_DIR}/src/apps/tftp/tftp.c
)
......
# LWIPAPPFILES: All LWIP APPs
set(lwipallapps_SRCS
${lwipsnmp_SRCS}
${lwiphttp_SRCS}
${lwipiperf_SRCS}
${lwipsmtp_SRCS}
${lwipsntp_SRCS}
${lwipmdns_SRCS}
${lwipnetbios_SRCS}
${lwiptftp_SRCS}
${lwipmqtt_SRCS}
)
......
# message(STATUS "@@@@@@@@lwip all app source files: ${lwipallapps_SRCS}")
add_library(lwipallapps EXCLUDE_FROM_ALL ${lwipallapps_SRCS})
target_compile_options(lwipallapps PRIVATE ${LWIP_COMPILER_FLAGS})
target_compile_definitions(lwipallapps PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})
target_include_directories(lwipallapps PRIVATE ${LWIP_INCLUDE_DIRS} ${LWIP_MBEDTLS_INCLUDE_DIRS})
example_app可执行文件cmake生成规则如下。
# cmake脚本位置 xxx\lwip\contrib\ports\win32\example_app\CMakeLists.txt
......
add_executable(example_app ${LWIP_DIR}/contrib/examples/example_app/test.c default_netif.c)
target_include_directories(example_app PRIVATE ${LWIP_INCLUDE_DIRS})
target_compile_definitions(example_app PRIVATE ${LWIP_DEFINITIONS} ${LWIP_MBEDTLS_DEFINITIONS})
message(STATUS "@@@@@@@@lwip all app library: ${lwipallapps}")
target_link_libraries(example_app ${LWIP_SANITIZER_LIBS} lwipallapps lwipcontribexamples lwipcontribapps lwipcontribaddons lwipcontribportwindows lwipcore lwipmbedtls)
......
3、确认使用MinGW-64生成的目标库是否正常。
由于在Visual Studio中编译完全正常,且在MinGW-64中相关库文件也正常生成了,可以确定库函数生成没有问题。
那么就只有库文件的链接规则可能存在问题了。
4、仔细查看example_app可执行文件的库链接规则。
target_link_libraries(example_app ${LWIP_SANITIZER_LIBS} lwipallapps lwipcontribexamples lwipcontribapps lwipcontribaddons lwipcontribportwindows lwipcore lwipmbedtls)
其中涉及到函数调用失败的2个库分别是 lwipallapps 和lwipcontribexamples,调用关系如下:
从上述关系中不难看出,库链接存在明显顺序问题。lwipallapps 库链接完成后 lwipcontribexamples 中相关依赖API符号可能已经失效,进而导致后续链接失败。
解决办法
1、直接使用Visual Studio。
这是官方原生支持的方式,也是最稳定可靠的方式,其中可能避免诸多其他错误,避免浪费大量精力和时间推荐这种方式。
2、修改链接库顺序。
修改后的链接顺序如下。
target_link_libraries(example_app ${LWIP_SANITIZER_LIBS} lwipcontribexamples lwipallapps lwipcontribapps lwipcontribaddons lwipcontribportwindows lwipcore lwipmbedtls)
再次执行编译,可以看到已经如期输出编译结果并成功进入了调试模式。
记录一些坑
使用lwip源码下的适配文件而不是老旧的兼容层
由于当前网络上关于lwip移植到windows上运行的教程较少,且大多数使用的版本较老,部分教程操作已经过期,对最新lwip已经不再适用。
-
独立contrib-2.1.0 包较旧,很多API已经不兼容,但是官网仍然在保留,如下图。
-
cmke文件组织结构发生了较大变化,很多指令操作不兼容,可能发生意想不到结果。
正确的文件结构
其实最新的lwip源码目录下已经提供了windows平台相关适配,基本不需要修改太多东西就能跑起来,平台适配层目录结构如下。
工程正确目录结构如下。
其中WpdPack静态库从WpdPack官网下载。注意同时也兼容npcap静态库,可从npcap官网下载。
指定的c标准在部分库编译过程中失效
现象就是编译部分库时虽然全局设置了c标准为C99,但依然报错提示在使用C90标准,导致部分注释格式,变量定义方式报错。
其原因是部分库说使用的编译标准依据一些条件发生了变化如下cmake规则所示。
# cmake脚本位置 xxx\lwip\contrib\ports\CMakeCommon.cmake
......
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
message(STATUS "@@@@@@@@:CMAKE 'GNU' mode. ")
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
-Wlogical-op
-Wtrampolines
)
if (NOT LWIP_HAVE_MBEDTLS)
message(STATUS "@@@@@@@@:CMAKE 'NOT LWIP_HAVE_MBEDTLS'. ")
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
$<$<COMPILE_LANGUAGE:C>:-Wc90-c99-compat> #这里重新指定了编译兼容标准
)
endif()
if(NOT CMAKE_C_COMPILER_VERSION VERSION_LESS 4.9)
if(LWIP_USE_SANITIZERS)
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
-fsanitize=address
-fsanitize=undefined
-fno-sanitize=alignment
-fstack-protector
-fstack-check
)
set(LWIP_SANITIZER_LIBS asan ubsan)
endif()
endif()
set(LWIP_COMPILER_FLAGS ${LWIP_COMPILER_FLAGS_GNU_CLANG})
endif()
找到相关内容,修改编译标准如下。
if(CMAKE_C_COMPILER_ID STREQUAL "GNU")
message(STATUS "@@@@@@@@:CMAKE 'GNU' mode. ")
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
-Wlogical-op
-Wtrampolines
)
if (NOT LWIP_HAVE_MBEDTLS)
message(STATUS "@@@@@@@@:CMAKE 'NOT LWIP_HAVE_MBEDTLS'. ")
# list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
# $<$<COMPILE_LANGUAGE:C>:-Wc90-c99-compat>
# )
# 设置编译器使用 C99 标准
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
-std=c99
)
endif()
if(NOT CMAKE_C_COMPILER_VERSION VERSION_LESS 4.9)
if(LWIP_USE_SANITIZERS)
list(APPEND LWIP_COMPILER_FLAGS_GNU_CLANG
-fsanitize=address
-fsanitize=undefined
-fno-sanitize=alignment
-fstack-protector
-fstack-check
)
set(LWIP_SANITIZER_LIBS asan ubsan)
endif()
endif()
set(LWIP_COMPILER_FLAGS ${LWIP_COMPILER_FLAGS_GNU_CLANG})
endif()
一些编译错误禁用技巧
编译过程中可能会遇到一些定义变量或函数未使用、类型转换被截断相关错误,可以适当添加一些标志去禁用。
VScode
- 修改cmake入参实现
{
"label": "CMake Configure",
"type": "shell",
"command": "cmake",
"args": [
"-S",
"${workspaceFolder}/contrib/ports/win32/example_app",
"-B",
"${workspaceFolder}/mingw_build",
"-G",
"MinGW Makefiles", // 根据你的系统选择合适的生成器,例如 "Unix Makefiles" 或 "Ninja"
"-DCMAKE_C_STANDARD=99",
"-DCMAKE_C_STANDARD_REQUIRED=ON",
// "-DCMAKE_C_FLAGS=-Wno-redundant-decls", // 添加忽略警告的标志
// "-DCMAKE_C_FLAGS=-Wno=redundant-decls"
// "-DCMAKE_C_FLAGS=-Wno=unused-parameter" // 忽略警告作为错误
"-DCMAKE_C_FLAGS=-Werror=missing-field-initializers",
"-DCMAKE_C_FLAGS=-Werror=unused-const-variable"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
],
"detail": "运行 CMake 配置命令"
}
其中 -DCMAKE_C_FLAGS=-Wno= 为禁用指定编译警告;-DCMAKE_C_FLAGS=-Werror= 为指定某些警告变成错误。
- 修改cmake编译标志实现
# cmake脚本位置 xxx\lwip\contrib\ports\win32\example_app\CMakeLists.txt
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-missing-field-initializers")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-const-variable")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-format-extra-args")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-format")
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wno-unused-parameter")
Visual Studio
-
全局调低编译警告等级
-
忽略指定警告
VScode调试过程中以16进制查看变量
变量监测格式:变量名,h 。如下图所示。
使用MinGW-64.gdb调试MSVC 编译的程序异常
具体表现为程序可以正常运行,但是设置的断点位置不会暂停,无法进行单步调试。可能需要使用MSVC对应的gdb程序才行,这里暂未做相关尝试。
附录
- VScode中编写的两种编译i器构建规则如下。
{
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
"version": "2.0.0",
"tasks": [
{
"label": "Build example_app",
"type": "shell",
"options": {
"cwd": "${workspaceFolder}/build"
},
"problemMatcher": "$gcc",
"group": {
"kind": "build",
"isDefault": true
},
"command": "cmake --build ."
},
{
"label": "Build unit tests",
"type": "shell",
"problemMatcher": "$gcc",
"group": "build",
"linux": {
"options": {
"cwd": "${workspaceFolder}/contrib/ports/unix/check/build"
},
},
"windows": {
"options": {
"cwd": "${workspaceFolder}/contrib/ports/win32/check/build"
},
},
"command": "cmake --build ."
},
{
"label": "Configure example_app",
"type": "shell",
"problemMatcher": "$gcc",
"group": "build",
"command": "cd ${workspaceFolder}; mkdir build; cd build; cmake .."
},
{
"label": "Generate documentation",
"type": "shell",
"problemMatcher": [],
"group": "none",
"options": {
"cwd": "${workspaceFolder}/build"
},
"command": "cmake --build . --target lwipdocs"
},
{
"label": "CMake Clean",
"type": "shell",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}/mingw_build", // 构建目录
"--target",
"clean" // CMake 清理目标
],
"group": "build",
"presentation": {
"echo": true,
"reveal": "always",
"focus": false,
"panel": "shared"
},
"problemMatcher": [],
"detail": "运行 CMake 清理命令,删除构建输出"
},
{
"label": "CMake Configure",
"type": "shell",
"command": "cmake",
"args": [
"-S",
"${workspaceFolder}/contrib/ports/win32/example_app",
"-B",
"${workspaceFolder}/mingw_build",
"-G",
"MinGW Makefiles", // 根据你的系统选择合适的生成器,例如 "Unix Makefiles" 或 "Ninja"
"-DCMAKE_C_STANDARD=99",
"-DCMAKE_C_STANDARD_REQUIRED=ON",
// "-DCMAKE_C_FLAGS=-Wno-redundant-decls", // 添加忽略警告的标志
// "-DCMAKE_C_FLAGS=-Wno=redundant-decls"
// "-DCMAKE_C_FLAGS=-Wno=unused-parameter" // 忽略警告作为错误
"-DCMAKE_C_FLAGS=-Werror=missing-field-initializers",
"-DCMAKE_C_FLAGS=-Werror=unused-const-variable"
],
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$gcc"
],
"detail": "运行 CMake 配置命令"
},
{
"label": "CMake Build",
"type": "shell",
"command": "cmake",
"args": [
"--build",
"${workspaceFolder}//mingw_build",
"--config",
"Debug" ,// 根据需要选择配置,例如 Release
"--parallel", // 并行编译
"4"
],
"dependsOn": "CMake Configure",
"group": {
"kind": "build",
"isDefault": true
},
"problemMatcher": [
"$msCompile",
"$gcc"
],
"detail": "运行 CMake 构建命令"
}
]
}
- GDB调试规则如下。
{
// 使用 IntelliSense 了解相关属性。
// 悬停以查看现有属性的描述。
// 欲了解更多信息,请访问: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "g++.exe - 生成和调试活动文件",
// type 告诉vscode编译器的类型,我用的MinGW64也就是g++,这里是cppdgb
// 这个是规定的,不是随便写,比如msvc编译器就是cppvsdbg
"type": "cppdbg",
"request": "launch",
// program 这个是你的可执行程序位置,这里可以根据自己的tasks.json生成
// 程序的位置自定义修改,等会参照后面的tasks.json内容
"program": "${workspaceFolder}\\mingw_build\\example_app.exe",
// ${xxxx}是vscode内置的变量,可以方便获取到需要的路径或者文件名,
// 具体什么变量参考别人的博客,
// 这里列举一部分
// ${workspaceFolder} :表示当前workspace文件夹路径,也即/home/Coding/Test
// ${workspaceRootFolderName}:表示workspace的文件夹名,也即Test
// ${file}:文件自身的绝对路径,也即/home/Coding/Test/.vscode/tasks.json
// ${relativeFile}:文件在workspace中的路径,也即.vscode/tasks.json
// ${fileBasenameNoExtension}:当前文件的文件名,不带后缀,也即tasks
// ${fileBasename}:当前文件的文件名,tasks.json
// ${fileDirname}:文件所在的文件夹路径,也即/home/Coding/Test/.vscode
// ${fileExtname}:当前文件的后缀,也即.json
// ${lineNumber}:当前文件光标所在的行号
// ${env:PATH}:系统中的环境变量
"args": [],
"stopAtEntry": false,
"cwd": "${workspaceFolder}",
"environment": [],
"externalConsole": true,
"MIMode": "gdb",
// 调试器的路径
"miDebuggerPath": "D:\\Program Files\\MinGW-W64\\bin\\gdb.exe",
"setupCommands": [
{
"description": "为 gdb 启用整齐打印",
"text": "-enable-pretty-printing",
"ignoreFailures": true
}
],
// preLaunchTask 表示在 执行调试前 要完成的任务
// 比如这里 要完成 makeRun 这个tasks任务(重新生成程序)
// 这里的 makeRun 是 tasks.json 中 lable 标记的任务名称
"preLaunchTask": "CMake Build",
}
]
}
总结
这是一个典型的静态链接库的依赖顺序问题。在链接静态库时,链接器会按照你在 target_link_libraries 中指定的顺序逐个扫描库文件,并只加载能够解决前面库中未定义符号的目标代码。由于 lwipcontribexamples 中调用了 lwipallapps 中的部分 API,所以必须保证 lwipcontribexamples 在 lwipallapps 之前出现,这样当链接器扫描到 lwipcontribexamples 时遇到未定义的符号,后面扫描到的 lwipallapps 才能提供相应的实现。如果你调换它们的顺序,链接器在扫描 lwipallapps 时就不会加载那些仅在后续出现的 lwipcontribexamples 中引用的符号,导致链接失败。
Visual Studio 的链接器与 GNU 等平台下的链接器在符号解析的处理方式上有所不同。MSVC 的链接器通常采用的是一种全局符号解析策略,它会收集所有目标文件和库中的符号,然后统一进行符号解析,从而不依赖库文件出现的先后顺序。这意味着,即使你调换了库的顺序,它也能在最终生成可执行文件时找到所有需要的符号。
相反,像 GNU ld 等很多链接器则采用单次扫描的方式,它们会按照你在命令行中给出的顺序依次处理库文件。如果某个库中的符号在后面的库中才被定义,且链接器在扫描时已经跳过了后面的库,就可能导致链接失败。因此,在这种情况下库的顺序就显得非常关键。
因此,你看到 VS Studio 能正确解析符号,而其他环境因链接顺序问题导致链接失败,正是由于各自链接器的解析策略不同所致。