• 通往WinDbg的捷径(一)


    通往WinDbg的捷径(一)
     
    原文:http://www.debuginfo.com/articles/easywindbg.html
    译者:arhat
    时间:2006413
    关键词:CDB WinDbg 
     
    导言
    你钟情什么样的调试器?如果你问我这个问题,我会回答是“Visual Studio + WinDbg”。我比较喜欢Visual Studio那朴实无华且易操作的接口,更喜欢它能迅速把我需要的信息以可视的形式展示出来。但遗憾的是,Visual Studio调试器无法获取某些信息。例如,假设我想知道哪个线程正在占用特殊的临界区?或者是哪个函数占用了大部分的栈空间?不用担心,有WinDbg呢。它的命令能回答这些问题,以及调试过程中出现的其它有趣的问题。甚至不退出Visual StudioWinDbg就可以附上目标应用程序――谢谢WinDbg支持入侵模式的调试(本文后面会详细讨论),我们可以把Visual Studio GUIWinDbg的命令行结合起来使用。
    唯一的问题是WinDbg不太好用。需要花些时间适应它的用户界面,而掌握它的命令则要花更多的时间。但是假设你现在就需要它,马上用它调试紧急的问题?有什么快速简便的方法吗?当然。WinDbg的小弟CDB,功能和WinDbg差不多;因为它是基于命令行的,所以用起来更简单一些。在这篇文章里,我将把CDB作为Visual Studio调试器的补充,介绍怎样使用CDB。在这篇文章里,你将会看到怎样配置CDB,怎样用它解决实际的问题。另外,我还会提供一些批处理文件,它们可以隐藏CDB命令行接口的大部分复杂性,也让你少打几个字。
     
    安装与配置
    安装
    当然,在使用CDB前,必须先安装并配置它。WinDbgCDBDebugging Tools for Windows 的一部分,可以从这里下载。安装很简单,你可以用默认设置安装,除非你准备用WinDbg SDK开发应用程序。(如果你准备用SDK,需要选择定制安装,并启用SDK安装;推荐你把它安装在不包含空格的目录名的目录中)。安装完成后,安装目录里将包含所有必需的文件,包括WinDbgwindbg.exe)和CDBcdb.exe)。
    调试工具也支持“xcopy”类型的安装。也就是说,在一台机器上安装后,如果你想在其它的机器上使用,不用再安装,直接把已经安装的目录直接拷过去就行了。
     
    符号文件服务器路径 
    如果不能访问操作系统DLL的最新的符号文件,有些重要的WinDbg命令将不能正常工作。在以往,我们可以从微软的FTP服务器上下载巨大的符号文件包,然后从中找出需要的符号文件。这非常浪费时间,而且在操作系统更新或升级后,符号文件就过时了(因此也就变得毫无用处)。幸运的是,现在有更简便的方法来获得符号文件――符号文件服务器。WinDbgVisual Studio都支持这个方法,在需要时直接从微软维护的服务上下载最新的符号文件。有了符号文件服务器,我们再也不用下载整个符号文件包了(那实在是太大了),因为调试器知道需要用到哪个DLLs,所以直接下载单个符号文件就行了。如果符号文件在操作系统更新或升级以后过时了,调试器会注意到这种情形,并再次下载必需的符号文件。
    为了使符号文件服务器起作用,我们应该让调试器知道符号文件服务器的路径。最简单的方法是在_NT_SYMBOL_PATH环境变量里指定符号文件服务器的路径。可以用如下的路径:
    "srv*c:"symbolcache*http://msdl.microsoft.com/download/symbols" 
    c:"symbolcache目录将被用来保存从符号文件服务器下载下来的符号文件;当然,你可以用任何有效的本地或网络路径)。例如:
     set _NT_SYMBOL_PATH=srv*c:"symbols*http://msdl.microsoft.com/download/symbols
    在你设置_NT_SYMBOL_PATH环境变量之后,就可以使用符号文件服务器了。关于符号文件服务器的更多信息,相关设置,以及可能会用到的排除故障的小技巧,可以从WinDbg的文档中找到(Debuggers | Symbols section)。
    如果你需要从一台需登录的代理服务器后访问符号文件服务器。参见本篇文章中CDB and proxy servers部分,以了解更多信息。
     
    CDB 命令行基础介绍 
    启动调试会话
    当我们使用新的调试器时,第一个问题通常是:怎样开始调试会话呢?像大多数调试器一样,CDB允许我们调试应用程序的新实例,或者附上一个已经运行的过程。启动新实例就象下面一样简单:
     cdb c:"myapp.exe
    如果我们想附上已经运行的过程,可能会用上下列某个选项:
    ----------------------------------------------------------------------------------------------------------------------
    选项                描述                                                                            例子
    ----------------------------------------------------------------------------------------------------------------------
    -p Pid              这个选项允许CDB附上指定进程ID的进程。可以用任务管理器或类似的工具得到进程ID   cdb -p 1034
    ----------------------------------------------------------------------------------------------------------------------
    -pn ExeName         这个选项允许CDB用指定的可执行文件名(.exe)附上进程。这个选项比“-p Pid”
                        方便,因为我们通常知道执行的程序名,不必在任务管理器中寻找进程的ID。但是如果
                        多个进程使用同一个名字(CDB将报错),就不能用这个选项了。                       cdb -pn myapp.exe
    ----------------------------------------------------------------------------------------------------------------------
    -psn ServiceName    这个选项允许CDB附上指定服务的进程。例如,假如你想附上Windows Management 
                        Instrumentation服务,应该用WinMgmt作为服务名。                                  cdb -psn MyService
    ----------------------------------------------------------------------------------------------------------------------
     
    CDB也可以分析故障转储。用-z选项打开故障转储:
     cdb -z DumpFile
    例如:
     cdb -z c:"myapp.dmp
     
    结束调试会话 
    启动新的调试会话后,CDB会显示它自己的命令行提示符。你可以在这个提示符下执行CDB支持的任何命令。
     
    'q'命令结束调试会话并退出CDB
    0:000> q
    quit:
    警告:当你结束调试会话,退出CDB时,操作系统也将终止被调试的程序。如果你想退出CDB并保持被调试程序,可以用.detach命令(Windows XP或更新的操作系统才支持),或者用非入侵的模式(下面讨论)。
     
    运行命令 
    虽然可以在CDB命令行提示符下执行调试器命令,但在命令行里指定需要的命令通常更快一些,用-c选项。
     cdb -pn myapp.exe -c "command1;command2"
    (用分号分隔多个命令)
    例如,下列命令行将把CDB附上我们的应用程序,显示已加载的模块,然后退出:
     cdb -pn myapp.exe -c "lm;q"
    注意,在命令列表的结尾加上'q'命令――将在所有的调试器命令执行后关闭CDB
     
    入侵模式调试
    在默认情况下,当我们用CDB调试一个已经运行的进程时,它通常作为全功能的调试器附上进程(使用Win32 Debugging API)。在这种模式下,可以设置断点,单步调试代码,得到各种调试事件的通知(例如,异常,加载/卸载模块,启动/退出线程,等等)。Visual Studio也可以做到这些,并提供更友好的用户界面。另外,每个进程每次只能被一个调试器附上。这是否意味着如果我们用Visual Studio调试器调试应用程序,就不能再用CDB得到它的附加信息了?不,不完全是这样,因为除了全功能调试模式外,CDB还支持入侵调试模式。
    CDB以入侵模式附上目标进程时,并没有使用Win32 Debugging API,而是先暂停目标进程的所有线程,执行用户指定的命令。在所有的命令执行之后,CDB退出之前,恢复暂停的线程。因此,目标进程可以继续运行,好像什么事也没发生一样。即使像Visual Studio之类的全功能调试器正在调试目标进程,CDB仍可以用入侵模式附上它,并获得所需要的信息。在CDB完成任务并分离附上的进程后,我们可以继续用Visual Studio调试器调试这个应用程序。
    怎么启用CDB的入侵模式?用-pv命令行选项。例如,下列命令行将以入侵模式附上应用程序,显示已加载模块的列表,然后退出。在CDB退出之后,应用程序将继续运行。
     cdb -pv -pn myapp.exe -c "lm;q"
     
    把输出内容保存到日志文件 
    有些CDB命令的输出内容可能会很长,从控制台窗口阅读十分不便。因此,把输出内容保存到日志文件,再用其它的编辑器查看会更好一些,CDB允许我们用-loga-logo选项来实现('-loga <filename>'把输出内容追加到指定文件的结尾;而'-logo <filename>'将覆盖原有的文件,如果文件已经存在的话)。
    在我们的例子命令(列出目标进程里的模块)里增加记录功能,把输出内容保存到当前目录的out.txt文件里:
     cdb -pv -pn myapp.exe -logo out.txt -c "lm;q"
     
    源行号信息 
    CDB支持的另外一个重要选项是-lines。这个选项打开源行号信息支持,例如,当报告调用栈时,允许CDB显示源文件及源行号。(在默认情况下,源行号支持是关闭的,CDB不显示源文件/行号信息)。
     
    CDB 和代理服务器 
    如果你在需要登录的代理服务器后用CDB,在默认情况下,将不能访问符号文件服务器。原因是在默认配置下,当CDB尝试连接符号文件服务器时,不显示代理服务器的登录提示。为了更改这个行为,使我们可以访问符号文件服务器,需要在命令行之前加上两条命令:
     !sym prompts;.reload
    例如:
     cdb -pv -pn myapp.exe -logo out.txt -c "!sym prompts;.reload;lm;q"
     
    启动消息
    CDB调试新应用程序,附上已经存在的进程,或打开故障转储时,将显示一系列的启动消息。CBD命令(可以用-c选项指定,或手动输入)的输出内容跟在这些消息之后。通常情况下,启动消息只显示一些无关紧要信息;但是如果在执行时出错了,它将包含这个问题的描述,有时候也会提供解决方法。
    例如,下列输出内容通知我们没有设置符号路径,因此,有些调试器命令不能工作:
    D:"Progs"DbgTools>cdb myapp.exe
     
    Microsoft (R) Windows Debugger Version 6.5.0003.7
    Copyright (c) Microsoft Corporation. All rights reserved.
     
    CommandLine: myapp.exe
    Symbol search path is: *** Invalid ***
    ****************************************************************************
    * Symbol loading may be unreliable without a symbol search path.           *
    * Use .symfix to have the debugger choose a symbol path.                   *
    * After setting your symbol path, use .reload to refresh symbol locations. *
    ****************************************************************************
     
    总结
    这里是一些常见的CDB命令行模板,本篇文章的剩下部分将会用到它们(我们总是用同样的模板,然后根据我们要解决的问题,改变-c选项内部的命令行列表)。
    用入侵模式附上运行的进程(通常是进程ID),执行一组命令,并把输出内容保存在out.txt文件里:
     cdb -pv -p <processid> -logo out.txt -lines -c "command1;command2;...;commandN;q"
    用入侵模式附上运行的进程(用可执行文件名),执行一组命令,并把输出内容保存在out.txt文件里:
     cdb -pv -pn <exename> -logo out.txt -lines -c "command1;command2;...;commandN;q"
    用入侵模式附上运行的进程(通常是服务名),执行一组命令,并把输出内容保存在out.txt文件里:
     cdb -pv -psn <servicename> -logo out.txt -lines -c "command1;command2;...;commandN;q"
    打开故障转储文件,执行一组命令,并把输出内容保存在out.txt文件里:
     cdb -z <dumpfile> -logo out.txt -lines -c "command1;command2;...;commandN;q"
    如果我们在需要登录的代理服务器后使用CDB,要访问符号文件服务器,需要增加两条命令。例如:
     cdb -pv -pn <exename> -logo out.txt -lines -c "!sym prompts;.reload;command1;command2;...;commandN;q"
    好像要打好多字?其实不是这样,稍后,我将提供一些批处理文件,它们将为我们隐藏重复的命令行选项,把要我们输入的内容减至最小。
     
    解决实际的问题
    调试死锁问题
    当我们的应用程序挂起或停止响应时,最自然的问题是:它现在正在做什么?它在哪里被困住了?当然,我们可以用Visual Studio调试器附上应用程序,检查所有线程的调用栈。但我们同样可以用CDB,而且会更快一些。下列命令将使CDB以入侵模式附上应用程序,打印所有的调用栈,把结果保存在日志文件里,然后退出:
     cdb -pv -pn myapp.exe -logo out.txt -lines -c "~*kb;q"
    'kb'命令要求CDB打印当前线程的调用栈;'~*'前缀要求CDB在进程所有已存在的线程里重复执行'kb'命令)。
    [/URL] DeadLockDemo.cpp是一个演示典型的死锁问题的例子。如果你编译并运行,它的工作线程马上会被困住,如果我们运行上述的命令来查看应用程序的线程正在做什么,将看到下列类似的内容(在这,以及后面,我们将省略启动消息):
    . 0 Id: 6fc.4fc Suspend: 1 Teb: 7ffdf000 Unfrozen
    ChildEBP RetAddr Args to Child              
    0012fdf8 7c90d85c 7c8023ed 00000000 0012fe2c ntdll!KiFastSystemCallRet
    0012fdfc 7c8023ed 00000000 0012fe2c 0012ff54 ntdll!NtDelayExecution+0xc
    0012fe54 7c802451 0036ee80 00000000 0012ff54 kernel32!SleepEx+0x61
    0012fe64 004308a9 0036ee80 a0f63080 01c63442 kernel32!Sleep+0xf
    0012ff54 00432342 00000001 003336e8 003337c8 DeadLockDemo!wmain+0xd9 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 154]
    0012ffb8 004320fd 0012fff0 7c816d4f a0f63080 DeadLockDemo!__tmainCRTStartup+0x232 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"crt0.c @ 318]
    0012ffc0 7c816d4f a0f63080 01c63442 7ffdd000 DeadLockDemo!wmainCRTStartup+0xd 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"crt0.c @ 187]
    0012fff0 00000000 0042e5aa 00000000 78746341 kernel32!BaseProcessStart+0x23
     
       1 Id: 6fc.3d8 Suspend: 1 Teb: 7ffde000 Unfrozen
    ChildEBP RetAddr Args to Child              
    005afc14 7c90e9c0 7c91901b 000007d4 00000000 ntdll!KiFastSystemCallRet
    005afc18 7c91901b 000007d4 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
    005afca0 7c90104b 004a0638 00430b7f 004a0638 ntdll!RtlpWaitForCriticalSection+0x132
    005afca8 00430b7f 004a0638 005afe6c 005afe78 ntdll!RtlEnterCriticalSection+0x46
    005afd8c 00430b15 005aff60 005afe78 003330a0 DeadLockDemo!CCriticalSection::Lock+0x2f 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 62]
    005afe6c 004309f1 004a0638 f3d065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 90]
    005aff6c 004311b1 00000000 f3d06511 00334fc8 DeadLockDemo!ThreadOne+0xa1 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 182]
    005affa8 00431122 00000000 005affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"threadex.c @ 348]
    005affb4 7c80b50b 003330a0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"threadex.c @ 331]
    005affec 00000000 00431080 003330a0 00000000 kernel32!BaseThreadStart+0x37
     
       2 Id: 6fc.284 Suspend: 1 Teb: 7ffdc000 Unfrozen
    ChildEBP RetAddr Args to Child              
    006afc14 7c90e9c0 7c91901b 000007d8 00000000 ntdll!KiFastSystemCallRet
    006afc18 7c91901b 000007d8 00000000 00000000 ntdll!ZwWaitForSingleObject+0xc
    006afca0 7c90104b 004a0620 00430b7f 004a0620 ntdll!RtlpWaitForCriticalSection+0x132
    006afca8 00430b7f 004a0620 006afe6c 006afe78 ntdll!RtlEnterCriticalSection+0x46
    006afd8c 00430b15 006aff60 006afe78 003332e0 DeadLockDemo!CCriticalSection::Lock+0x2f 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 62]
    006afe6c 00430d11 004a0620 f3e065d5 00334fc8 DeadLockDemo!CCritSecLock::CCritSecLock+0x35 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 90]
    006aff6c 004311b1 00000000 f3e06511 00334fc8 DeadLockDemo!ThreadTwo+0xa1 
      [c:"tests"deadlockdemo"deadlockdemo.cpp @ 202]
    006affa8 00431122 00000000 006affec 7c80b50b DeadLockDemo!_callthreadstartex+0x51 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"threadex.c @ 348]
    006affb4 7c80b50b 003332e0 00334fc8 00330001 DeadLockDemo!_threadstartex+0xa2 
      [f:"rtm"vctools"crt_bld"self_x86"crt"src"threadex.c @ 331]
    006affec 00000000 00431080 003332e0 00000000 kernel32!BaseThreadStart+0x37
    调用栈(和源行号)暗示ThreadOne正在占用临界区CritSecOne并等待临界区CritSecTwo,然而ThreadTwo正占用临界区CritSecTwo并等待临界区CritSecOne。这是典型的“lock acquisition order”死锁例子,在那里,两个线程需要得到同一组同步的对象,以不同的顺序使用。如果你想避免这种类型的死锁,必须保证所有的线程以相同的顺序得到所需的同步对象(在这个例子里,ThreadOneThreadTwo能同意首先得到CritSecOne,然后得到CritSecTwo来避免死锁)。
    在默认情况下,'kb'命令只显示调用栈的前20帧。如果你想查看更多的栈帧,你可以显式指明显示的栈帧数量(例如,'kb100'命令要求调试器显示100帧)。在WinDbg会话里,可以用.kframes命令改变随后命令的默认限制。
    我们的例子只包含了三个简单的线程,很容易看出哪个线程应该为死锁负责。在大应用程序里,很难找出可疑的线程并进行验证。那我们应该怎么做呢?在大部分情况下,我们应该知道那个没有正常运转的线程(否则,我们怎么会注意到应用程序出现异常了呢?)。通常,这个线程是在等待同步对象,这个对象因为某些原因暂时不可用。这个对象为什么不可用呢?如果我们知道哪个线程正在占用这个对象(拥有它,换句话说),应该能答出这个问题。如果这个对象碰巧在临界区,!locks命令应该能帮助我们识别出它的当前所有者。当不带参数使用时,这条命令显示应用程序线程正在占用的临界区的列表。输出的内容不包括已释放的临界区。
    让我看看实际使用中的!locks命令:
     cdb -pv -pn myapp.exe -logo out.txt -lines -c "!locks;q"
    下面是这条命令的输出内容(同样以DeadLockDemo.cpp为例):
    CritSec DeadLockDemo!CritSecOne+0 at 004A0620
    LockCount          1
    RecursionCount     1
    OwningThread       3d8
    EntryCount         1
    ContentionCount    1
    *** Locked
     
    CritSec DeadLockDemo!CritSecTwo+0 at 004A0638
    LockCount          1
    RecursionCount     1
    OwningThread       284
    EntryCount         1
    ContentionCount    1
    *** Locked
     
    仔细查看了40个临界区
    查看!locks命令的输出(尤其是OwningThread字段),我们可以推断出临界区CritSecOneID0x3d8的线程占用,临界区CritSecTwoID0x284的线程占用。我们可以在'kb'命令的输出内容(在前面的输出里)里找出这些IDs对应的线程。
    如果应用程序使用其它种类的同步对象(例如,互斥),识别它们的所有者将更难一些(需要内核调试器),我准备在以后的文章中再介绍这部分内容。
     
    调试CPU高消耗的问题 
    对大多数软件来说,太高的CPU消耗率(根据任务管理器的显示,在单CPU上接近100%)明显指出软件中有bug。通常意味着应用程序的某个线程陷入了死循环。当然,调试这个问题的、最普通的方法是用Visual Studio调试器附上这个进程,查找哪个线程在捣乱。但是我们应该检查哪个线程呢?CDB为我们提供了简便的方法――!runaway命令。当不带参数使用时,这条命令显示应用程序每个线程执行用户模式代码时所花的时间(使用另外的参数,可以显示在内核模式下所花的时间,自线程启动后占用的时间等)。
    如下是在CDB下使用这条命令的示例:
     cdb -pv -pn myapp.exe -logo out.txt -c "!runaway;q"
    下面是!runaway命令的输出示例:
    0:000> !runaway
     User Mode Time
     Thread       Time
       1:358       0 days 0:00:47.408
       2:150       0 days 0:00:03.495
       0:d8        0 days 0:00:00.000
    看起来好像是ID0x358的线程占用了大部分的CPU时间。但这个消息还不足以证明线程0x358就是罪魁祸首,因为这条命令显示的CPU时间是线程在它整个生命期中所花的。我们还需要进一步查看线程所用CPU时间的变化情况。让我们再次运行这条命令。这次,我们可以看到类似于下列的内容:
    0:000> !runaway
     User Mode Time
     Thread       Time
       1:358       0 days 0:00:47.408
       2:150       0 days 0:00:06.859
       0:d8        0 days 0:00:00.000
    现在,我们可以把这个输出内容与上次的输出内容做个比较,找出CPU时间增长最快的线程。在这个例子里,很明显就是线程0x150。现在,我们可以用Visual Studio调试器附上这个应用程序,切换到这个线程下,检查它为什么转个不停。
     
    调试栈溢出
    当我们想找出栈溢出异常的原因时,CDB也非常有帮助。当然,无控制的递归调用是栈溢出最典型的原因,通常来说,查看损坏了的线程的调用栈,找出它从哪里脱离控制就可以了。Visual Studio在这方面可以做的很好,那为什么还要用CDB呢?让我们设想一个更复杂的例子。例如,假设我们的应用程序中包含一个依赖递归的算法?我们在设计算法时使用有符号数,在所有可能的情形下控制递归的运行,但某个时候栈仍溢出了。为什么?或许是因为在某种情况下,算法使用的某些函数占用了太多的栈空间。我们怎么确定函数占用的总的栈空间呢?不幸地是,Visual Studio调试器没有简便的方法可以做到。
    即使调用栈没有显示任何递归的迹象时,应用程序也可能会出现栈溢出异常。例如,查看StackOvfDemo.cpp例子。如果你编译,并在调试器下运行它,将立刻出现栈溢出。但此刻的调用栈看起来一切正常:
    StackOvfDemo.exe!_woutput
    StackOvfDemo.exe!wprintf
    StackOvfDemo.exe!ProcessStringW
    StackOvfDemo.exe!ProcessStrings
    StackOvfDemo.exe!main
    StackOvfDemo.exe!mainCRTStartup
    KERNEL32.DLL!_BaseProcessStart@4
    显然,调用栈上的某个函数使用了太多的栈空间。但是我们怎么找出这个函数呢?不用担心,有了CDB'kf'命令的帮助,可以显示每个函数在调用栈上占用的字节数。在应用程序还停在Visual Studio调试器里的时候,我们可以运行下列命令:
     cdb -pv -pn stackovfdemo.exe -logo out.txt -c "~*kf;q"
    'kf'默认显示调用栈上最后的20帧,像我们在调试死锁问题部分讨论的那样。如果你想多显示一些,可以增加前缀,例如,~*kf1000。另外要注意的是,~*kf将报告所有线程的调用栈。如果应用包含大量的线程,它就不太适合了,这时,可以把它改成'~~[tid]kf', 'tid'是目标线程的线程ID(例如,'~~[0x3a8]kf'))
    这条命令显示的内容如下:
    . 0 Id: 210.3a8 Suspend: 1 Teb: 7ffde000 Unfrozen
     Memory ChildEBP RetAddr 
              00033440 0041aca5 StackOvfDemo!_woutput+0x22
           44 00033484 00415eed StackOvfDemo!wprintf+0x85
           d8 0003355c 00415cc5 StackOvfDemo!ProcessStringW+0x2d
        fc878 0012fdd4 00415a44 StackOvfDemo!ProcessStrings+0xe5
          108 0012fedc 0041c043 StackOvfDemo!main+0x64
           e4 0012ffc0 7c4e87f5 StackOvfDemo!mainCRTStartup+0x183
           30 0012fff0 00000000 KERNEL32!BaseProcessStart+0x3d
    注意第一列的内容――它报告栈上函数所占用的字节数。很显然,ProcessStrings函数用了可用栈空间的最大份额,因此,它可能要为栈溢出负责。
     
    如果你想知道ProcessStrings函数为什么需要如此多的栈空间,这里有一些解释。这个函数使用ATLA2W宏把字符串从ANSI格式转换成Unicode格式,这个宏在内部用_alloca函数在栈上分配内存。用_alloca分配的内存只有当它的调用者(在这个例子里是ProcessStrings)返回后才被释放。直到ProcessStrings返回控制之前,A2W(因此,也就是_alloca)在栈上为每个后续的调用分配另外的空间,这将迅速耗尽栈空间。
    底线:不要在循环里使用_alloca

     

  • 相关阅读:
    javascript和C#比较
    前端模块管理器简介
    javascript中的splice方法介绍&示例
    javascript中数组揭秘
    17款code review工具
    IIS ip访问限制插件
    iis 限制动态IP地址访问次数
    AWS云使用100条宝贵经验分享
    C# 开源框架(整理)
    如何获取Azure AD tenant的tenant Id?
  • 原文地址:https://www.cnblogs.com/holly/p/1409940.html
Copyright © 2020-2023  润新知