0

0

Windows中Loader Lock引起的死锁问题

爱谁谁

爱谁谁

发布时间:2025-09-17 08:31:21

|

604人浏览过

|

来源于php中文网

原创

在程序开发中,常见的做法是将程序模块化,通常实现为动态链接库(dll)。在主程序启动时,可以通过隐式或显式的方式加载这些动态链接库。然而,在windows系统中,如果动态链接库的dllmain函数编写不当,可能会导致一些意想不到的bug,例如典型的loader lock死锁问题。这是一个许多windows开发者可能遇到的陷阱。

  1. 背景介绍

当主程序启动时,无论是通过隐式还是显式的方式加载动态链接库,都会调用动态链接库的DllMain函数。同样,当创建新线程时,线程启动过程中也会隐式地调用DllMain。为了确保多个线程按顺序调用DllMain,微软在内部使用了一个称为Loader Lock的锁,这个锁作用于整个进程。

例如,当使用LoadLibrary首次加载动态链接库时,调用顺序如下:

Windows中Loader Lock引起的死锁问题

由于存在这个隐式的Loader Lock,在编写DllMain时需要特别小心。以下是《Windows核心编程》书中20.2.5节的一个死锁示例:

BOOL WINAPI DllMain(HINSTANCE hInstDll, DWORD fdwReason, PVOID fImpLoad){
  HANDLE hThread;
  DWORD dwThreadId;
  switch (fdwReason){
    case DLL_PROCESS_ATTACH:
      // DLL被映射到进程的地址空间
      hThread = CreateThread(NULL, 0, SomeFuction, NULL, 0, &dwThreadId);
      WaitForSingleObject(hThread, INFINITE);
      CloseHandle(hThread);
      break;
    case DLL_THREAD_ATTACH:
      // 创建新线程
      break;
    case DLL_THREAD_DETACH:
      // 线程正常退出
      break;
    case DLL_PROCESS_DETACH:
      // DLL从进程的地址空间中卸载
      break;
  }
  return TRUE;
}

从上述示例可以看出,当DLL接收到DLL_PROCESS_ATTACH通知时,会创建一个新线程。系统会用DLL_THREAD_ATTACH通知新创建的线程调用DllMain。然而,由于之前的线程还在DllMain中等待新创建的线程执行完毕,并且占用了Loader Lock,新创建的线程将无法获得Loader Lock,从而导致死锁。

  1. Windbg分析问题

在背景介绍中,我们了解到Loader Lock可能会导致一些隐含的Bug,因此在编写DllMain时需要格外小心。实际项目中的情况可能比上述示例更为复杂,但理解了原理后,问题的分析会更接近真相。以下是简化的一个实际项目中出现问题的逻辑:

Windows中Loader Lock引起的死锁问题

产品以Windows服务的形式存在,启动服务时首先加载A.dll。在A.dll的DllMain中创建一个线程Thread2(如果该线程接收到清理Log的Event,将会对Log进行清理)。然后加载B.dll,在B.dll的DllMain中检查log文件,如果其大小超过10M,则通知Thread2清理log,并等待Thread2完成清理(最多等待5分钟)。然而,当log文件超过10M时,启动服务有时会出现启动超时的情况。

Baklib
Baklib

在线创建产品手册、知识库、帮助文档

下载

于是,我们使用Windbg附加到hang的主进程上,首先查看哪些锁被占用:

0:019> !locks
CritSec ntdll!LdrpLoaderLock+0 at 0000000077d17490
WaiterWoken        No
LockCount          12
RecursionCount     1
OwningThread       cb0
EntryCount         0
ContentionCount    d
*** Locked

可以看到,锁被线程cb0(十六进制)占用,并且从LockCount来看,还有许多线程在请求Loader Lock。使用"!thread"命令获取占用Loader Lock的线程cb0的顺序号为5(下面只列出了6个线程,实际上有几十个线程):

0:019> !threads
Index  TID      TEB        StackBase      StackLimit      DeAlloc      StackSize      ThreadProc
0  0000000000000d4c  0x000007fffffdd000  0x0000000000130000  0x0000000000126000  0x0000000000030000  0x000000000000a000  0x0
1  0000000000000fc0  0x000007fffffdb000  0x0000000002490000  0x000000000248e000  0x0000000002390000  0x0000000000002000  0x0
2  0000000000000968  0x000007fffffae000  0x0000000002cc0000  0x0000000002cbe000  0x0000000002bc0000  0x0000000000002000  0x0
3  0000000000000914  0x000007fffffac000  0x0000000002dc0000  0x0000000002dbe000  0x0000000002cc0000  0x0000000000002000  0x0
4  0000000000000de4  0x000007fffffaa000  0x0000000002ec0000  0x0000000002ebc000  0x0000000002dc0000  0x0000000000004000  0x0
5  0000000000000cb0  0x000007fffffa8000  0x0000000002fc0000  0x0000000002f9a000  0x0000000002ec0000  0x0000000000026000  0x0

然后查看线程cb0的函数调用栈,它在xmodule3模块的DB_xxxxxxxxx函数中hang。这个函数就是之前提到的,通知清理log的线程并等待其完成清理(最多等待5分钟),这个线程正在等待。

0:019> ~5kv
Child-SP          RetAddr           : Args to Child                                                           : Call Site
00000000`02fbd558 000007fe`fdd81203 : 00000000`02fbd618 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!NtDelayExecution+0xa
00000000`02fbd560 00000000`63151a35 : 00000000`00000008 00000000`00000000 00000000`00000000 00000000`00000000 : KERNELBASE!SleepEx+0xab
00000000`02fbd600 00000000`6327299d : 00000000`00000000 00000000`00000000 00000000`00000010 00000000`002e2770 : xmodule3!DB_xxxxxxxxx+0x105
00000000`02fbd650 00000000`007fab85 : 00000000`00000001 00000000`00000004 00000000`00000268 00000000`02fbe3a8 : xmodule2!LM_xxxxx+0x18d
00000000`02fbe3e0 00000000`0082848d : 00000000`00000001 00000000`00000001 00000000`00000000 000012eb`e9b70b34 : xmodule1!ENG_xx+0x605
00000000`02fbee10 00000000`77c1b108 : 00000000`002cbb00 00000000`00000000 00000000`00000000 00000000`00297bf4 : xmodule1!ENG_xxx+0x2065d
00000000`02fbee50 00000000`77c0787a : 00000000`00000000 00000000`002cbb00 00000000`02fbef60 00000000`00000000 : ntdll!LdrpRunInitializeRoutines+0x1fe
00000000`02fbf020 00000000`77c07b5e : 00000000`00000000 00000000`0012fc38 00000000`02fbf2c0 000007fe`fdd8da2d : ntdll!LdrpLoadDll+0x231
00000000`02fbf230 000007fe`fdd89059 : 00000000`00000000 00000000`00000000 00000000`0012fc38 00000000`00000046 : ntdll!LdrLoadDll+0x9a
00000000`02fbf2a0 00000001`40003b05 : 00000000`00000000 00000000`0012fc38 00000001`4000e3d8 00000000`00000000 : KERNELBASE!LoadLibraryExW+0x22e
00000000`02fbf310 00000000`757237d7 : 00000000`0096d840 00000000`0096d840 00000000`00000000 00000000`00000000 : XXXSvc+0x3b05
00000000`02fbff00 00000000`75723894 : 00000000`757d95c0 00000000`0096d840 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x47
00000000`02fbff30 00000000`779d652d : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : MSVCR80!endthreadex+0x104
00000000`02fbff60 00000000`77c0c541 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : kernel32!BaseThreadInitThunk+0xd
00000000`02fbff90 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!RtlUserThreadStart+0x1d

从上述信息可以看出,线程cb0一直在等待清理log的线程完成清理。那么,清理log的线程到底发生了什么呢?首先,log中记录了清理log线程的句柄为"17c"(十六进制)。查看其线程Id为5fc.890。

0:019> !handle 17c f
Handle 17c
  Type           Thread
  Attributes     0
  GrantedAccess  0x1fffff:
         Delete,ReadControl,WriteDac,WriteOwner,Synch
         Terminate,Suspend,Alert,GetContext,SetContext,SetInfo,QueryInfo,SetToken,Impersonate,DirectImpersonate
  HandleCount    4
  PointerCount   6
  Name           
  Object Specific Information
    Thread Id   5fc.890
    Priority    10
    Base Priority 0
    Start Address 75723810 MSVCR80!endthreadex

使用相同的方法查看清理log线程的函数调用栈,发现它在"ntdll!RtlpWaitOnCriticalSection"中,参数"00000000`77d17490"恰好是Loader Lock。终于真相大白了~~

0:019> ~6kv
Child-SP          RetAddr           : Args to Child                                                           : Call Site
00000000`0321f858 00000000`77c2e518 : 00000000`00000000 00000000`00000194 000007ff`fffa62c8 00000000`77c0c4fa : ntdll!ZwWaitForSingleObject+0xa
00000000`0321f860 00000000`77c2e40b : 00000000`00000001 000007ff`fffdf000 00000000`77be0000 00000000`77d17490 : ntdll!RtlpWaitOnCriticalSection+0xe8
00000000`0321f910 00000000`77c0c5dd : 00000000`00000000 000007ff`fffa6000 000007ff`fffa62c8 00000000`00000000 : ntdll!RtlEnterCriticalSection+0xd1
00000000`0321f940 00000000`77c0c44f : 000007ff`fffdf000 00000000`00000000 000007ff`fffa6000 00000000`00000000 : ntdll!LdrpInitializeThread+0x8d
00000000`0321fa40 00000000`77c0c34e : 00000000`0321fb00 00000000`00000000 000007ff`fffdf000 00000000`00000000 : ntdll!LdrpInitialize+0x9f
00000000`0321fab0 00000000`00000000 : 00000000`00000000 00000000`00000000 00000000`00000000 00000000`00000000 : ntdll!LdrInitializeThunk+0xe

了解问题的根源后,解决这个问题也变得相对简单。通过这个案例,我们得到的启示是,尽量避免在DllMain中实现过多的逻辑。可以使用一个导出函数,在加载动态链接库之后,手动调用这个导出的初始化函数。

热门AI工具

更多
DeepSeek
DeepSeek

幻方量化公司旗下的开源大模型平台

豆包大模型
豆包大模型

字节跳动自主研发的一系列大型语言模型

通义千问
通义千问

阿里巴巴推出的全能AI助手

腾讯元宝
腾讯元宝

腾讯混元平台推出的AI助手

文心一言
文心一言

文心一言是百度开发的AI聊天机器人,通过对话可以生成各种形式的内容。

讯飞写作
讯飞写作

基于讯飞星火大模型的AI写作工具,可以快速生成新闻稿件、品宣文案、工作总结、心得体会等各种文文稿

即梦AI
即梦AI

一站式AI创作平台,免费AI图片和视频生成。

ChatGPT
ChatGPT

最最强大的AI聊天机器人程序,ChatGPT不单是聊天机器人,还能进行撰写邮件、视频脚本、文案、翻译、代码等任务。

相关专题

更多
堆和栈的区别
堆和栈的区别

堆和栈的区别:1、内存分配方式不同;2、大小不同;3、数据访问方式不同;4、数据的生命周期。本专题为大家提供堆和栈的区别的相关的文章、下载、课程内容,供大家免费下载体验。

394

2023.07.18

堆和栈区别
堆和栈区别

堆(Heap)和栈(Stack)是计算机中两种常见的内存分配机制。它们在内存管理的方式、分配方式以及使用场景上有很大的区别。本文将详细介绍堆和栈的特点、区别以及各自的使用场景。php中文网给大家带来了相关的教程以及文章欢迎大家前来学习阅读。

575

2023.08.10

线程和进程的区别
线程和进程的区别

线程和进程的区别:线程是进程的一部分,用于实现并发和并行操作,而线程共享进程的资源,通信更方便快捷,切换开销较小。本专题为大家提供线程和进程区别相关的各种文章、以及下载和课程。

502

2023.08.10

Python 多线程与异步编程实战
Python 多线程与异步编程实战

本专题系统讲解 Python 多线程与异步编程的核心概念与实战技巧,包括 threading 模块基础、线程同步机制、GIL 原理、asyncio 异步任务管理、协程与事件循环、任务调度与异常处理。通过实战示例,帮助学习者掌握 如何构建高性能、多任务并发的 Python 应用。

144

2025.12.24

java多线程相关教程合集
java多线程相关教程合集

本专题整合了java多线程相关教程,阅读专题下面的文章了解更多详细内容。

5

2026.01.21

C++多线程相关合集
C++多线程相关合集

本专题整合了C++多线程相关教程,阅读专题下面的的文章了解更多详细内容。

11

2026.01.21

Java 并发编程高级实践
Java 并发编程高级实践

本专题深入讲解 Java 在高并发开发中的核心技术,涵盖线程模型、Thread 与 Runnable、Lock 与 synchronized、原子类、并发容器、线程池(Executor 框架)、阻塞队列、并发工具类(CountDownLatch、Semaphore)、以及高并发系统设计中的关键策略。通过实战案例帮助学习者全面掌握构建高性能并发应用的工程能力。

83

2025.12.01

windows查看端口占用情况
windows查看端口占用情况

Windows端口可以认为是计算机与外界通讯交流的出入口。逻辑意义上的端口一般是指TCP/IP协议中的端口,端口号的范围从0到65535,比如用于浏览网页服务的80端口,用于FTP服务的21端口等等。怎么查看windows端口占用情况呢?php中文网给大家带来了相关的教程以及文章,欢迎大家前来阅读学习。

730

2023.07.26

拼多多赚钱的5种方法 拼多多赚钱的5种方法
拼多多赚钱的5种方法 拼多多赚钱的5种方法

在拼多多上赚钱主要可以通过无货源模式一件代发、精细化运营特色店铺、参与官方高流量活动、利用拼团机制社交裂变,以及成为多多进宝推广员这5种方法实现。核心策略在于通过低成本、高效率的供应链管理与营销,利用平台社交电商红利实现盈利。

4

2026.01.26

热门下载

更多
网站特效
/
网站源码
/
网站素材
/
前端模板

精品课程

更多
相关推荐
/
热门推荐
/
最新课程
PostgreSQL 教程
PostgreSQL 教程

共48课时 | 7.8万人学习

Excel 教程
Excel 教程

共162课时 | 13.5万人学习

PHP基础入门课程
PHP基础入门课程

共33课时 | 2万人学习

关于我们 免责申明 举报中心 意见反馈 讲师合作 广告合作 最新更新
php中文网:公益在线php培训,帮助PHP学习者快速成长!
关注服务号 技术交流群
PHP中文网订阅号
每天精选资源文章推送

Copyright 2014-2026 https://www.php.cn/ All Rights Reserved | php.cn | 湘ICP备2023035733号