用cygwin64的make在Windows下编译darknet,明明makefile写得没问题,代码也是官方代码,偏偏跑到mingw64那一步就崩了,报一堆undefined reference to '__imp_shutdown'、__imp_socket之类的错。这个场景我太熟了,当初为了在Windows上把darknet折腾起来,光环境就换了三轮,最后一个坑就是这个问题。网上搜一圈,答案七零八碎,有说换成VS编译的,有说别用cygwin的,但都没讲到点子上。今天专门把这个问题掰开揉碎讲清楚,不光告诉你改哪一行能过,更要把为什么报错、为什么这样改能修好讲明白,免得下次换个项目又卡在同一个地方。
1. 先搞明白:为什么cygwin的make会调出mingw64的编译错误
1.1 这个报错的真实含义
undefined reference to '__imp_shutdown',翻译成人话就是:链接器在编译darknet的某个源文件时,发现你调用了shutdown这个Windows Socket函数(用来关闭套接字的),但是链接阶段找不到这个函数的实现。前面带__imp_前缀,说明这是一个从DLL导入的符号——也就是说,编译器明确知道你用的是Windows系统自带的ws2_32.dll里的socket函数,但在链接的时候没把对应的导入库ws2_32.lib(或者libws2_32.a)给喂进去。
听起来像是缺库的问题,但很多人把-lws2_32写进makefile之后依然报同样的错,这就不是简单缺库的问题了。
1.2 根子出在:三套工具链在混着用
这里有个关键背景:darknet官方代码默认是奔着Linux去的,makefile里的默认编译器是gcc,Windows下想要在纯命令行环境编译,有两个流派——cygwin和mingw64。cygwin的gcc编译出来的程序依赖cygwin1.dll,提供的是POSIX风格的接口;mingw64的gcc编译出来的程序直接用Windows原生API,不依赖额外的POSIX模拟层。
问题就出在:你用cygwin64自带的make去运行darknet的makefile,但环境变量PATH里把mingw64的bin目录也加进去了,或者makefile里显式把CC指向了mingw64的gcc。这一混,编译阶段用的可能是mingw64的头文件,链接阶段可能又把cygwin的库路径、mingw的库路径搅在一起。Windows socket那套东西,cygwin有一套自己的实现方式,mingw是走windows SDK的方式,两边的符号修饰规则还不一样。于是在某个目标文件的链接过程里,链接器拿着mingw版的目标文件去找cygwin风格的socket导入符号,或者反过来,自然就找不到。
1.3 一个关键认知:正常编译流程应该是什么样的
在Windows上成功编译darknet,应该满足这几个条件:
- 编译器、头文件、库文件,全部来自同一套工具链,不能cygwin的gcc配mingw的头文件,也不能mingw的gcc去链cygwin的库
- socket库的链接参数必须显式给出:
-lws2_32,因为darknet的makefile是按Linux写的,Linux下socket函数直接进libc,不需要额外链库,但Windows必须把ws2_32.lib或libws2_32.a链进来 - 如果用了cygwin的gcc整体编译,那全程都走cygwin的规则,socket符号一般能自动处理好;如果用了mingw64的gcc整体编译,那就必须保证库里能对上
__imp_*这组符号
所以,遇到__imp_shutdown报错,本质上不是某个函数的问题,而是工具链混用导致的系统性冲突。理解了这一点,后面所有解决方案都围绕着同一个目标:让整个编译过程只用一套工具链,并且让链接器拿到正确的socket导入库。
2. 解决方案全景:三条路线怎么选
针对这个报错,实际能走通的路有三条,各有各的适用场景,直接给结论:
| 方案 | 核心做法 | 适合场景 | 踩坑指数 |
|---|---|---|---|
| 方案A:回归纯MinGW-w64工具链 | 把make、gcc、链接器全部换成MinGW-w64版本,完全抛弃cygwin | 想用一个干净、原生Windows环境,不想被cygwin依赖拖累 | 中等 |
| 方案B:cygwin全家桶编译 | 不使用mingw64,全程用cygwin的gcc和cygwin的make | 已经装了cygwin,而且不介意程序依赖cygwin1.dll | 低 |
| 方案C:修正makefile,强制链接ws2_32 | 保留现有环境,在makefile的LDLIBS里加-lws2_32并调整库顺序 | 只图快速跑通darknet,后续还要在自己代码里改东西 | 中高,因为治标不治本 |
我个人的建议是:如果是第一次搞,直接走方案A,一套干净的MinGW-w64环境能省掉后面80%的莫名其妙问题。如果你已经熟练了,或者必须要一个在别的机器上能拷过去直接跑的程序,那方案B更合适——cygwin环境编译出来的exe在没装cygwin的机器上跑不了,mingw64编译出来的exe基本可以裸跑(只要目标机器有对应VC运行时)。darknet默认编译是不需要额外runtime的。
先别急着动手,讲一下为什么方案A最稳。MinGW-w64是从MinGW发展而来的,专门针对Windows x64做了大量优化,支持的API更新更全。darknet里有大量pthread、socket、时间戳操作,这些在MinGW-w64里都有对应的原生Windows实现,不会跑到一半发现某个头文件缺了。而cygwin的gcc虽然也能力强,但它编译产物依赖cygwin1.dll,你要是在别的机器上部署,还得到处拷贝这个dll,很麻烦。
3. 方案A实操:纯MinGW-w64从零编译darknet(全程无cygwin依赖)
3.1 环境准备:下载和安装MinGW-w64
这一步最简单也最容易出错,很容易被网上各种教程带到沟里。记住一个原则:不要去下载那些打着“MinGW”旗号的老版本,要选MinGW-w64的x86_64版本,并且要带POSIX线程模型的那个。
下载MinGW-w64建议直接找官方渠道或者知名的集成包。安装时注意三件事:
- 架构选x86_64
- 线程模型选posix(不是win32),因为darknet的代码用了pthread,如果选win32线程模型,虽然也能编出来,但某些pthread接口的行为有差异,容易出现奇怪的问题
- 异常处理模型选seh(64位下性能好)或sjlj(兼容性更强)。darknet编译建议sjlj,兼容性最好,但如果你打算拿这个编译器去编其他大型项目,seh更好
装完之后把mingw64/bin目录加到系统PATH的最前面,保证命令行里敲gcc --version出来的是MinGW-w64的gcc,而不是别的什么编译器。
3.2 给makefile动手术:三处必改
darknet的makefile本身是Linux向的,直接拿到Windows上编译,光改socket链接还不行,还有几个地方要一起改:
第一处,把行尾的-lm保留,但要新增-lws2_32。Linux下数学库和socket都在系统库里,Windows下-lm对应的是mingw自带的数学库,socket必须单独链接。找到makefile里类似LDLIBS = -lm -pthread这一行,改成:
LDLIBS = -lm -pthread -lws2_32第二处,检查有没有显式指定的CC和CFLAGS。有时候makefile里写着CC = gcc,没毛病,但如果你之前为了别的原因把CC显式改成了带路径的/usr/bin/gcc或者C:/cygwin64/bin/gcc,那你就是自己把工具链搞混了。确保这里留白或者直接写成gcc,让系统PATH决定用哪个gcc。
第三处,darknet的makefile里如果有VPATH或依赖.so、.dylib之类Linux风格的动态库配置,Windows下要么注释掉,要么改成.dll。你还会发现AR = ar,这个在mingw下就是ar也可以,不用动。
改完的三行差不多长这样:
CC = gcc LDLIBS = -lm -pthread -lws2_32 CFLAGS = -Wall -Wno-unknown-pragmas -Wfatal-errors -fPIC3.3 还可能跳出来的其他未定义引用:不是只有shutdown
老实说,__imp_shutdown只是第一只出头鸟,改完这个再编,你大概率还会看到其他:
undefined reference to 'gettimeofday'——Windows没有这个POSIX函数,需要额外处理undefined reference to 'usleep'——同样的POSIX专属函数undefined reference to '__imp_htonll'或__imp_ntohll——大端序转换函数在较老的Windows SDK里没有
这些都是darknet代码在Linux上跑惯了、默认平台有这些函数导致的。解决办法有两个层面。
如果只求编译通过,可以在代码前面统一加一套Windows兼容层。darknet源码里其实已经有一些#ifdef _WIN32的片段,但覆盖不全。一个通用的做法是在darknet.h或utils.h里加一段:
#ifdef _WIN32 #include <winsock2.h> #include <windows.h> #include <time.h> static inline int gettimeofday(struct timeval *tp, void *tzp) { FILETIME ft; ULARGE_INTEGER ul; GetSystemTimeAsFileTime(&ft); ul.LowPart = ft.dwLowDateTime; ul.HighPart = ft.dwHighDateTime; ul.QuadPart -= 116444736000000000ULL; tp->tv_sec = (long)(ul.QuadPart / 10000000ULL); tp->tv_usec = (long)(ul.QuadPart % 10000000ULL) / 10; return 0; } #endifusleep更简单,直接:
#ifdef _WIN32 #include <windows.h> #define usleep(us) Sleep((us) / 1000) #endifhtonll在没有新SDK的编译器里,可以自己写字节序翻转:
#ifdef _WIN32 static unsigned __int64 htonll_windows(unsigned __int64 value) { if (1 == htons(1)) return value; return ((value & 0xFFULL) << 56) | ((value & 0xFF00ULL) << 40) | ((value & 0xFF0000ULL) << 24) | ((value & 0xFF000000ULL) << 8) | ((value & 0xFF00000000ULL) >> 8) | ((value & 0xFF0000000000ULL) >> 24) | ((value & 0xFF000000000000ULL) >> 40) | ((value & 0xFF00000000000000ULL) >> 56); } #define htonll htonll_windows #define ntohll htonll_windows #endif这些padding看着多,但都是编译大型跨平台项目必备的基础功,别嫌麻烦。
3.4 生成weights文件和运行exe
编译出来darknet.exe之后,如果测试目标检测,还要把预训练的weights文件下载下来放到对应目录。这里有个小坑:darknet读取weights是按相对路径来的,你的当前工作目录必须跟你执行./darknet detect ...命令的目录一致,否则会报Couldn't open file。
命令行测试:
./darknet detect cfg/yolov4.cfg yolov4.weights data/dog.jpg第一次跑会初始化CUDA(如果你编译了GPU版本)或者纯CPU跑,CPU版在Windows下速度还行,但比GPU版差一个数量级,这点要有心理准备。
4. 方案B实操:cygwin全家桶到底能不能编译成功
4.1 保持纯cygwin环境的关键设置
如果坚持用cygwin,那就要把“纯”字贯彻到底。首先,确保PATH里没有mingw64的任何路径。然后需要在cygwin安装器里额外装几个包:
gcc-core和gcc-g++:cygwin的编译器libgcc1:运行时库make:cygwin版的GNU makepkg-config:可能用到libpthread相关:darknet需要
这些在cygwin的setup程序里都能勾选,注意搜索的时候别选成mingw64交叉编译器版本(名字里带mingw64的那种),要选纯gcc-core的。
4.2 cygwin下的makefile到底有什么区别
cygwin模拟了大部分Linux API,所以darknet的makefile几乎可以不动,-lm、-lpthread这些在cygwin下都能自动对到对应实现。但有一个地方要注意:socket。在cygwin下,如果你链接的代码里用了socket函数,其实大部分情况不用加-lws2_32,因为cygwin把socket封装进了自己的网络层。但是有时候你还是会遇到类似的undefined reference,这时候在makefile里加上:
LDLIBS += -lws2_32cygwin的gcc也能识别这个参数,因为cygwin提供了一个兼容层,能把Windows的socket API包装成POSIX风格。这里有个学问:cygwin下-lws2_32链接到的库是cygwin自己包装的那个版本,而不是mingw的版本,符号规则是符合cygwin的,所以不会出错。这点恰恰解释了为什么方案B的踩坑指数最低——cygwin把底层的脏活都承包了。
4.3 编译产物依赖cygwin1.dll的注意事项
走方案B编译出来的darknet.exe,你把exe拷到别的机器上直接双击,大概率提示找不到cygwin1.dll。解决办法两种:
一种是把cygwin的bin目录下的cygwin1.dll、cygpthread-1.dll、cyggcc_s-1.dll等一起拷到exe同目录,或者拷到系统目录。这个方法最简单,但是几个dll加起来可能几十MB,而且随着程序功能增加,依赖的dll会越来越多。
另一种是编译时加-static,把cygwin的库静态链接进exe:
CFLAGS += -static LDLIBS += -static这样exe体积会暴涨到几百MB级别,但换来了单一文件分发,适合部署到没有cygwin环境的机器。
我的经验是:本地快速验证用默认动态链接,发布给别人用的时候再加-static重新编一遍,两全其美。
5. 方案C实操:保留双环境,但只改makefile能不能救
5.1 只加-lws2_32为什么有时候依然不行
网上最常见的回复就是“在LDLIBS加-lws2_32”,但我一开始就说了,这个方案有时候还是不生效。原因藏在链接顺序里。GNU的链接器在做符号解析时是“单遍扫描”的,也就是说,目标文件和库文件的出现顺序,决定了链接器能不能“回头”找到之前没解析的符号。
darknet的makefile里通常长这样:
OBJ = obj1.o obj2.o obj3.o ... all: $(OBJ) $(CC) $(CFLAGS) -o darknet $(OBJ) $(LDLIBS)关键就是最后的$(OBJ)和$(LDLIBS)的相对位置。如果$(OBJ)里某个目标文件引用了shutdown,那么链接器必须在此后某个库文件中找到shutdown。所以体系必须是:先有引用方(obj),后有提供方(lib)。如果LDLIBS的位置写在了OBJ前面,就完蛋——链接器扫描到obj时,发现shutdown未定义,但库文件已经被扫过了,链接器不会回头,直接给你报undefined reference警告/错误。
所以一个稳妥的写法是:
LDLIBS = -lm -pthread -lws2_32 $(CC) $(CFLAGS) -o darknet $(OBJ) $(LDLIBS) -Wl,--start-group $(LDLIBS) -Wl,--end-group--start-group和--end-group的作用是让链接器在库之间做循环扫描,直到所有能解析的符号都解析完。这个方法在多个库互相依赖时尤其有用。但这也是个双刃剑——它会增加链接时间,而且某些场景下可能掩盖真正的循环依赖问题,新手调试时会很困惑。
5.2 另一种方式:直接在源文件里强制包含winsock头文件
如果你能接受改源码,并且你对工具链的混用问题不太想管,试试在darknet的main文件或者网络相关文件顶部,强制先包含Windows头文件:
#ifdef _WIN32 #ifndef WIN32_LEAN_AND_MEAN #define WIN32_LEAN_AND_MEAN #endif #include <winsock2.h> #include <ws2tcpip.h> #endif为什么要强调WIN32_LEAN_AND_MEAN?因为如果不定义这个宏,windows.h会额外引入很多Windows特有的头文件,经常和winsock2冲突。darknet的代码里可能包含了windows.h的间接引用,先定义WIN32_LEAN_AND_MEAN可以避免一堆重复定义错误。
5.3 方案C适合什么人
说实话,方案C是最不推荐长期使用的,因为你等于在带病运行。工具链混用带来的问题不止socket一个,后面还有strtok、ssize_t、pthread_t等一堆POSIX和Windows的语义差异等着爆雷。每次爆雷你都要去补一堆补丁,编译配置会越来越臃肿。这个方案只适合:你已经在一台机器上把环境折腾得差不多了,改几行能编译过去就先跑着,下一阶段再换干净环境。
6. 实操过程中最高频的10个排查问题
把多年在Windows下编译darknet或其他C项目的经验浓缩成一张速查表,你照着这个顺序排查,比自己瞎试快得多。
| 现象 | 最常见原因 | 排查动作 |
|---|---|---|
| make命令都敲不了,提示不是内部或外部命令 | cygwin的bin没加到PATH | echo %PATH%看有没有cygwin路径,没有就加 |
| gcc版本显示的是w64/cigwin之类分不清 | 多个编译器混在PATH里 | 敲where gcc看命中的是哪个路径,把不用的移走 |
编译到一半报找不到pthread.h | mingw的线程模型选错了 | 重装MinGW-w64,选posix模型 |
编译能过,链接报__imp_socket/__imp_shutdown | 缺ws2_32库或库顺序错误 | 确认LDLIBS里-lws2_32在最尾部,或加--start-group |
链接报大量_imp___ZN...之类C++符号找不到 | 编译器用了不同版本的C++运行时 | 检查所有.o是不是同一个gcc编译的,不要混新旧版本 |
| exe能跑但一打开网络功能就闪退 | WSAStartup没有调用 | darknet的demo/检测代码如果用到网络,确保第一步调用WSAStartup |
| 编译darknet的CUDA版本,nvcc报错 | CUDA版本和gcc版本不匹配 | 查对应CUDA版本的gcc兼容列表,Windows下一般建议CUDA自带的主机编译器 |
cfg文件读不了,报Bad cfg | 行尾符是Linux格式或UTF-8 BOM带进去了 | 用编辑器统一转成LF行尾,去掉BOM |
| 检测图片时框出来但标签乱码 | 字体文件路径不对 | darknet默认读取data/下的字体文件,按cfg里的路径调整 |
| exe在别人电脑上跑不了/缺DLL | 动态依赖没处理好 | 用Dependency Walker类工具看一下缺失的DLL,补dll或静态编译 |
这里专门讲一下WSAStartup。很多人编译通过了,但一跑./darknet detector demo ...或者任何需要网络操作的命令就崩溃,原因就是Windows下任何用到winsock的程序,第一件事必须是调用WSAStartup来初始化socket环境。Linux下不需要这个步骤,所以darknet的代码里没有默认加。你可以自己写个小包装,或者做一个简单的初始化调用:
#ifdef _WIN32 WSADATA wsaData; WSAStartup(MAKEWORD(2, 2), &wsaData); #endif放在main函数最开始的位置,能救回一大半的运行时崩溃。
7. 从这个问题延伸出去:Cygwin/MinGW的“世界观”差异
7.1 符号前缀和“ABI”到底是什么
很多人被__imp_这个前缀搞懵了。这里给你打个比方:Windows下从DLL导入的函数,在编译时会被改名为__imp_函数名,链接器看到这个符号就知道“哦,这是要从某个DLL导入的”,然后去对应的导入库(.lib或.a)里找真正的跳转地址。MinGW通过这样的机制来对接Windows DLL生态。而cygwin不这么干,cygwin维护了一套自己的符号表,把Windows的socket API包装成POSIX风格,所以cygwin下不会出现__imp_前缀的符号。这也是为什么混用时经常出现“明明链接了ws2_32,还是找不到__imp_shutdown”——你链接的库是cygwin风格的符号,但目标文件是mingw风格编译的,两者语言不通。
7.2 为什么darknet这种项目最容易踩坑
darknet源码量中等,但它的特性是大量直接调用系统API,包括pthread、socket、文件锁、时间函数,而且没有专门维护Windows的构建脚本。对比那些像OpenCV一样积极拥抱跨平台的项目,darknet的makefile就是Linux味十足。所以编译darknet的Windows版本,本质上就是一次完整的“把一个Linux项目移植到Windows”的训练。你搞定了darknet,以后再遇到其他C项目在Windows编译报错,很多都是同一套排查逻辑。
7.3 个人建议:环境选择要前置,不要临时抱佛脚
如果你是一个打算长期在Windows下搞深度学习模型部署的开发者,建议尽早统一环境标准。两条主流路线:
- 路线一:MSYS2全家桶。MSYS2自带pacman包管理器,可以一键装mingw-w64工具链,环境干净,库齐全,比手动装cygwin+mingw好维护得多。darknet在里面编也顺。
- 路线二:WSL。直接装个Linux子系统的Ubuntu,在WSL里编darknet,速度不慢,而且完全避开Windows工具链的坑。唯一的问题是WSL里面没法直接调GPU做训练(除非格外折腾),但编译代码、测试CPU版本完全够用。
我后来在Windows下编译darknet的推荐组合是:MSYS2 + MinGW-w64 + Make,这套组合既保留了命令行操作的快感,又绕开了cygwin的依赖库地狱。
8. 最后分享一个藏在细节里的经验
编译darknet这类项目,大多数人会盯着makefile改来改去,但我到后来才发现,很多“玄学报错”其实是文件编码和行尾符的问题。darknet源码从GitHub克隆下来一般是LF行尾,用记事本打开保存之后就变成CRLF行尾,某些编译器对CRLF处理不当,会导致宏定义失效、条件编译错乱,进而产生一些看起来毫无关联的链接错误。遇到奇怪报错时,先回想一下自己有没有用记事本、某些编辑器编辑过源文件。如果编辑过,先把行尾统一转回LF,#include和宏定义相关的诡异错误往往就消失了。这是Windows开发最容易忽略、也最容易让人怀疑人生的细节。