解决在windows平台调试lwip协议中时使用MinGW-W64工具链链接源码过程中遇到部分库函数引用失败问题

应用场景

需要在windows平台编译lwip源码进行源码仿真调试,做一些协议细节方面验证工作。由于Visual Studio 开发环境过大,打算利用vscode + cmake + MinGW-64搭建基础环境。

软件版本

软件版本
lwip2.2.1
cmake3.22.1
MinGW-W648.1.0
WpdPack4.1.2
Visual Studio202217.12.3

lwip最新源码从github下载。

遇到的问题

在 VSCode 使用 MinGW-W64 编译lwip源码过程中部分库函数 [undefined reference to `tftp_init_client’]
MinGW-64 链接库函数失败
使用微软的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个库分别是 lwipallappslwipcontribexamples,调用关系如下:

call
link
lwipcontribexamples
lwipallapps

从上述关系中不难看出,库链接存在明显顺序问题。lwipallapps 库链接完成后 lwipcontribexamples 中相关依赖API符号可能已经失效,进而导致后续链接失败。

解决办法

1、直接使用Visual Studio。

这是官方原生支持的方式,也是最稳定可靠的方式,其中可能避免诸多其他错误,避免浪费大量精力和时间推荐这种方式。

2、修改链接库顺序。

修改后的链接顺序如下。

target_link_libraries(example_app ${LWIP_SANITIZER_LIBS} lwipcontribexamples lwipallapps lwipcontribapps lwipcontribaddons lwipcontribportwindows lwipcore lwipmbedtls)

再次执行编译,可以看到已经如期输出编译结果并成功进入了调试模式。

MinGw64编译成功

记录一些坑

使用lwip源码下的适配文件而不是老旧的兼容层

由于当前网络上关于lwip移植到windows上运行的教程较少,且大多数使用的版本较老,部分教程操作已经过期,对最新lwip已经不再适用。

  • 独立contrib-2.1.0 包较旧,很多API已经不兼容,但是官网仍然在保留,如下图。
    lwip兼容层老旧

  • cmke文件组织结构发生了较大变化,很多指令操作不兼容,可能发生意想不到结果。

正确的文件结构

其实最新的lwip源码目录下已经提供了windows平台相关适配,基本不需要修改太多东西就能跑起来,平台适配层目录结构如下。
lwip平台适配层
工程正确目录结构如下。
工程正确目录结构
其中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 。如下图所示。
vscode调试16进制查看变量

使用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 能正确解析符号,而其他环境因链接顺序问题导致链接失败,正是由于各自链接器的解析策略不同所致。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值