一篇超老的文章,但很有用,文风朴实易读,怕以后找不到,保存在这里 October 1997


Introducing the Bugslayer: Annihilating Bugs in an Application Near You

John Robbins

The Debug Run-Time Library offers great features: memory overwrite, underwrite, and freed memory access checking; memory leak checking; memory allocation hook; user-defined memory block dumping; and clean asserting and reporting macros. Plus, many of the features are extensible!
This article assumes you're familiar with C, C++

Code for this article: Bugs.exe (53KB)

John Robbins is a software engineer at NuMega Technologies, Inc., who specializes in debuggers. He can be reached at john@jprobbins.com.
Bugs are the coolest, most excitingthings in the whole wide world! If you are an entomologist, you just might agree with that statement. Any competent software engineer—and I am 99.999 percent sure that you are since you're reading this—would probably wonder what kind of drugs I'm on. Granted, bugs are definitely not cool when they cause you to stay up all night trying to figure them out, or worse yet, when they cause you to lose your job because too many customers send a buggy product back. Proactively finding and fixing bugs long before your product gets to the customer can be very satisfying—especially since you won't have to spend the last half of the development cycle sitting in a debugger wondering where the problems are. 
Welcome to The Bugslayer! This article introduces my new column in MSJ. In my column, I will discuss ways that you can find and fix as many bugs as possible before the customer ever sees your product. Since bugs can creep into your product anywhere from design to final ship, there is a great deal of ground to cover. My goal is to provide information that will help you build automatic bug killing right into your products. While a few columns might contain higher-level discussions, most will focus on particular areas where bugs are found—for example, multithreading. And following theMSJ tradition, they will offer tools, techniques and a bunch of source code you can use. 
Now that I have broadly outlined the column, here is where you come in. I want to cover the topics you are most interested in seeing, so let me know what your interests are. While some bug-solving techniques are generic across all environments, others aren't. I want to try and hit those that are most interesting to the most folks. If you have debugging or bug-squashing questions or ideas, drop me a line atjohn@jprobbins.com. I look forward to hearing from you. Your Free Lunch
When Visual C++® 4.0 first arrived on my doorstep, it had all of those nice new features, but in my mind one small little addition was coolest: the Debug Run-Time Library. The strange thing is, most folks don't seem to realize that it exists because much of it is turned off by default. Once you get the proper flags switched on, however, it offers many great features: memory overwrite, underwrite, and freed memory access checking; memory leak checking; memory allocation hook; user-defined memory block dumping; and clean asserting and reporting macros. Plus, many of the features are extensible! In this article, I will extend the debug runtime so that you get even more functionality out of it. 
The first library I developed, MemDumperValidator, provides a generic mechanism for hooking into the memory-dumping part of the debug runtime. So, when there is an error with your allocated memory, you get to see exactly what was in that memory. It also sets up a scheme to allow you to validate everything inside a memory block. The second library I developed, MemStressLib, hooks into the allocation portion of the debug runtime so that you can selectively fail memory allocations and test how your program handles them. 
Since Microsoft is nice enough to provide the complete runtime source code, all the functionality for the debug runtime is easily seen. In the CRT\SRC directory, all the work takes place in the following files: 

DBGDEL.CPP The debug global delete operator.
DBGHEAP.C All of the debug heap-handling functions.
DBGHOOK.C The stub memory allocation hook function.
DBGINT.H The internal debug headers and functions.
DBGNEW.CPP The debug global new operator.
DBGRPT.C The debug reporting functions.
CRTDBG.H The header file you include. This is in the standard include directory.

The debug runtime is full of features, but I want to concentrate on the memory tracking and checking features that it offers. The first step to using the debug runtime is to include the main header CRTDBG.H, where all the functionality is defined. Right before you do the actual include, you will need to define _CRTDBG_MAP_ALLOC. This gets the allocation routines mapped to special versions that record the source and line of the call, which really helps give you some extra information about where things happened. 
After you get the debug runtime included, you have to get it turned on. The documentation says that most of the debug runtime is turned off to keep the code small and to increase execution speed. While this may be important for a release build, the whole point of a debug build is to find bugs! The increased size and reduced speed of debug builds is inconsequential. The _CrtSetDbgFlag function takes a set of flags, shown inFigure 1, that turns on various options in the debug runtime. If you want to use either of the libraries, I included CRTDBG.H and defined _CRTDBG_ MAP_ALLOC for you. Both libraries make it a snap to get the full debug runtime library turned on.
Now that you have the debug runtime fully available, you get a slew of functions that really help you control memory usage. One of the most useful functions that you can call is _CrtCheckMemory. This function walks through all of the memory you have allocated and checks to see if you have any underwrites or overwrites and if you have used any blocks that were previously freed. This one function alone makes the entire debug runtime worth using. 
But wait, there's more! Another set of functions allows you to easily check the validity of any piece of memory. The _CrtIsValidHeapPointer, _CrtIsMemoryBlock, and _CrtIsValidPointer functions are perfect for using as debugging parameter validation functions. If they are wrapped in ASSERT macros, they become doubly useful. While these, combined with _CrtCheckMemory, offer sufficient memory checking, there's still more! 
Another neat feature of the debug runtime is the memory state routines. _CrtMemCheckpoint, _CrtMemDifference, and _CrtMemDumpStatistics make it easy to do before and after comparisons of the heap to see if anything is amiss. For example, if you are using a common library in a team environment, you could take before and after snapshots of the heap when you call the library to see if there are any leaks, or to see how much memory is used on the operation. 
The icing on the memory-checking cake is that the debug runtime allows you to hook into the memory allocation code stream so you can see each allocation and deallocation function call. If the allocation hook returns TRUE, the allocation is allowed to continue. If the allocation hook returns FALSE, then the allocation will fail. My immediate thought was that, with a small amount of work, I could have a means to test code in some really nasty boundary conditions that would be very difficult to duplicate. Fortunately, you will not have to do that work because it's handled by MemStressLib, one of the libraries that I will present later. 
The cherry on top of the icing of the memory-checking cake is that the debug runtime allows you to hook the memory dumping routines and to enumerate client blocks (your allocated memory). With the memory dumping, you can now hook in a dump routine that knows about your data, so that instead of seeing the default cryptic dumped memory, which is not very helpful, you can see exactly what the memory block contains and format it exactly as you want. MFC has the Dump function for this purpose, but it only works with CObject-derived classes. If you're like me, you don't spend your entire coding life in MFC and you need dumping functions that are more generic to accommodate different types of code. 
The client enumeration, as the name implies, allows you to enumerate the memory blocks you have allocated. This means you have an excellent opportunity for some interesting utilities. In the MemDumperValidator library, I combined the dumping hooks so the enumeration can dump and validate many types of allocated memory. The validation is very important when you consider that this extensible validation allows you to do "deep" validation instead of the surface checks of underwrites and overwrites. By deep, I mean something that knows about what is in the memory block so it can truly make sure that everything is correct. 
I have just highlighted the debug runtime here so you'll more easily understand the libraries that I am about to present. There is a great deal of value in the debug runtime, and the best part is that, in conjunction with the MemStress and MemDumperValidator libraries, you get all of your cake and a fork to help you eat it. But before I jump right into the code, I need to explain a little bit about how things are initialized in Visual C++®. Just How Are Things Initialized and Terminated in C++?
In my MemDumperValidator library, I take advantage of a small trick to get everything initialized by the compiler long before you use the library, and terminated long after the program is finished executing your code. While I could have you call initialization and shutdown functions to tell MemDumperValidator to start and stop, your calls could happen too late and too early, respectively, if you have any static C++ classes that use MemDumperValidator. Static C++ classes are constructed before main/WinMain is called and destructed after main/WinMain returns, so it might be rather difficult for you to figure out when to do the initialization and shutdown calls. My goal is to make the library as automatic as possible so you can just use it without spending a lot of time figuring it out. Also, controlling the initialization order is something that you do not think about much, but when it is wrong it takes some serious debugging effort to get right. 
What I need is a way to tell the compiler to call my initialization routines before it calls the ones in my code, and to call my termination routines after it calls those in my code. The documentation refers to this as the initialization order; the #pragma init_seg is how you can control it. There are several "parameters" that you can pass to the init_seg directive: compiler, lib, user, section name, and func-name. The first three are the important ones.
The compiler directive is reserved for the Microsoft compiler, and any objects specified for this group are constructed first and destructed last. Those that are marked as lib are constructed next and destructed before the compiler-marked group, and those marked user are constructed last and terminated first. 
Since the code that I developed needs to be initialized before your code, I could just specify lib as the directive to init_seg and be done with it. However, if you are creating libraries and marking them as lib segments (as you should) and want to use my code, my code still needs to be initialized before your code. To handle this contingency, I set the init_seg directive as "compiler." While I would not suggest you do this with release-build code, it is safe enough with debug code.
Since the initialization idea only works with C++ code, MemDumperValidator uses a special static class that simply calls the initializer functions for the libraries. The initializer function is a little more complex than just setting a couple of variables to known values, which is why I need to go to all of this trouble. Additionally, as discussed below, to get around some limitations in the debug runtime memory leak checking, I need to do some special processing on the class destruction. Even though the library only has a C interface, I can take advantage of C++ to get everything lined up so it is ready when you call it.
Using Memory Dumper and Validator Library
As I mentioned earlier, the stock memory dumping routines could be improved because they just display the first couple of bytes of the block and the address. The MemDumperValidator library allows you to hook into the debug runtime memory dumping; you can get nicely formatted output of exactly what is in your memory, so you have an idea what is in the block you are leaking or corrupting. When debugging, any extra piece of information is power.
In addition to memory dumping, the validator portion gives you the means not only to check for overwrites and underwrites, but a clean way to do deep validation of everything in the memory block. Before I delve into the inner workings, I will discuss the high-level view and how to use the library. If you want to follow along with the code, Figure 2 shows the header file MemDumperValidator.h.
The MemDumperValidator takes advantage of the debug runtime block identifier capabilities so that it can associate a block type to a set of routines that knows something about what is in the block. After you set up a class or C data type to use the MemDumperValidator library, the library will be called when the debug runtime wants to dump a block. The library will look at the block value, and if there is a matching dumping function, it will call it to dump the memory. The validation portion will do the same thing when called by the debug runtime, except it calls the validation function. As usual, describing it is easy, but getting it all to work is a little more difficult.
Setting up a C++ class so it can be handled by the MemDumperValidator library is a relatively simple operation. In the declaration of the C++ class, just specify the DECLARE_MEMDEBUG macro with the class name as the parameter. This macro is rather like some of the magic MFC macros in that it expands into a couple of data and method declarations. If you are following along in MemDumperValidator.h, you will notice that there are three inline functions: new, delete, and new with placement syntax. If you have any of these three operators in your class, then you will need to extract what these functions do and place it in your code.
In the implementation file for your C++ class, you need to use the IMPLEMENT_MEMDEBUG macro, again with your class name as the parameter. This sets up a static variable for your class. The IMPLEMENT_MEMDEBUG and DECLARE_MEMDEBUG macros only expand in _DEBUG builds, so they do not need to have conditional compilation used around them.
After you have specified both macros in the correct place, you will only need to implement the two functions that will do the actual dumping and validation for your class. The prototypes for those functions are shown below. Obviously, you will want to put some conditional compilation around them so they are not compiled into release builds.

static void ClassDumper ( const void * pData ) ;static void ClassValidator ( const void * pData,const void * pContext  ) ;
For both functions, the pData parameter is the actual memory block that points to an instance of the class. All that is needed to get to a usable pointer is to cast the value in pData to the class type. Whatever you do when you are dumping or validating, treat the value in pData as super read-only or you could cause yourself major headaches. For the ClassValidator method, the second parameter, pContext, is the context parameter that was passed to the original call to the ValidateAllBlocks function. This is discussed in more detail later.
I have only two recommendations for implementing the dump function. First, stick to the _RPTn macros so that your formatted output will go to the same place as the rest of the runtime debug output. Second, end your output with a carriage return/line feed combination.
While it looks almost trivial to set up a dumper and validator for a C++ class, what about those C data structures that would be nice to finally get dumped cleanly? Unfortunately, it takes a little more work to handle them. To see all the steps in action, follow the C examples in Dump.cpp (see Figure 3).
The first step is to set the unique memory block values for your C data structures. The C++ macros automatically handle this. The specific values that you will want to assign for each individual class must be defined in terms of the CLIENT_BLOCK_VALUE macro, which takes a single integer value as a parameter. If you are going to have several C data structure dumpers and validators, it might be a good idea to define all the memory block values in the same header file since they all must be unique.
While the C++ macros automatically declare the dumper and validator functions for you, you will need to manually declare them for your C memory. The prototypes for the C dumper and validator functions are the same as the C++ versions except that the static keyword is not used. Like declaring the unique memory block values, you might want to consider placing all the C memory dumping and validation function implementations in a combined file for convenience.
Before you can start allocating, dumping, or validating C memory, you must tell the MemDumpValidator library about the block type and the dumper and validator functions for it. This is handled with the INITIALIZE_ MEMDEBUG macro that takes the assigned block value, the dump function, and the validate function as parameters. You will need to have the macro executed before you allocate any memory blocks for this type.
Finally—and this is where the C++ memory handling is far better—to allocate, free, reallocate, expand, or get the size of a block with msize, you must use a whole set of macros that pass the block value through to the underlying memory function. For example, if your block value is defined as MYBLOCKVAL, then you need to allocate your C blocks with:
MEMDEBUG_MALLOC(MYBLOCKVAL,sizeof(x))
 Figure 2 shows all the different macros for the C memory functions. Since it can get rather confusing to remember the block type for each and every type of allocation, you might want to set up wrapper macros to handle it for you, so that all you need to pass to your wrapper macros are the normal parameters to the memory functions.
While the dumping portion of this library is unquestionably useful, you might be wondering why you need the validation method, even if it does allow you to do deep validation into the memory block. In many cases, the validation function might even be an empty function if all the class holds is a couple of string variables. But it can be invaluable because it gives you some excellent debugging capabilities. One of the first things that I started using it for was to provide a second level of data validation on a set of base classes that I had developed. While this should not replace good old fashioned parameter and input checking, it gave me another layer of assurance that everything was correct.
The neatest thing to use the validation function for is double-checking complex data structures after performing operations on them. For example, one time I had a relatively complex situation where two separate self-referential data structures both used the same allocated objects because of space considerations. After filling in the data structures with a large set of data, I used the validate function to look at the individual blocks from the heap and check that they were referentially correct. While I could have written a bunch of code to walk each data structure, any code that you write is an open target for bugs. By using the validate function, I could bounce through the allocated blocks using code that had already been tested, and got to check the data structures from different positions because it was in the order of allocation.
While it is a little more difficult to get C allocations set up, at least using the memory validation function is the same in both C and C++. All it takes is calling the VALIDATEALLBLOCKS macro. This expands in _DEBUG builds to a call to the ValidateAllBlocks routine. The parameter required is any value that you would like to get passed on through to the validation functions that you registered with the library. I have used this in the past to determine the depth of the validation that the function will perform. Keep in mind that this value is passed to every registered validation routine, so you might need to coordinate the values across your team.
To see the MemDumperValidator library in action, check out the Dump program shown in Figure 3. It is a stripped-down program that just shows you what you need to use the library. Now let's look at how it is implemented. While I did not provide a code example, the MemDumperValidator library works quite well with MFC because MFC will call any previously registered client dump hook functions. This way you can get the best of both worlds!
The Memory Dumper and Validator Library Implementation
The implementation for the MemDumperValidator library is contained in MemDumperValidator.cpp (seeFigure 4). What makes the library work is the unique block value for each type of memory allocated. The debug runtime lets you assign unique values to the memory allocated by your program, which it refers to as client blocks. Since I wanted the library to be usable for C code as well as C++ code, the library keeps the values unique with the CheckMaxSubType function.
While allowing you to assign unique values to your allocated memory is a great feature of the debug runtime, there is only one small problem: the debug runtime does not have a documented way to get the block value from the hook functions. The hook functions are only passed a pointer to the user data, not the whole block the debug runtime allocates. Fortunately, with the source code to the runtime library I was able to see exactly how the debug runtime allocates memory blocks. They are all allocated as a _CrtMemBlockHeader structure defined in the DBGINT.H file. 
Also in the file are macros to get at the _CrtMemBlockHeader from the user data pointer and to get the user data from a _CrtMemBlockHeader. I copied the _CrtMemBlockHeader structure and access macros into a header file, CRTDBG_Internals.h (see Figure 5), so I could get at the header information. While this is not a good practice, it works because the debug runtime _CrtMemBlockHeader structure did not change between Visual C++ 4.0 and Visual C++ 5.0. That doesn't mean that it won't change in a future version.
One other nice thing about pulling the _CrtMemBlockHeader structure definition out is that you can use it to get more information from the _CrtMemState structures returned by _CrtMemCheckpoint because the first item in the structure is a pointer to a _CrtMemBlockHeader. Hopefully, a future version of the debug runtime will give us real access functions to get the memory block information.
With the gyrations needed to get the unique client block value figured out, the rest of the library is relatively simple. The unique client block value for the block and the dumper and validator functions are stored in a DVINFO structure. The global data structure in MemDumperValidator.cpp, g_pstDVInfo, is an array of DVINFO structures in block value order. Storing the values in this array means that you just have to use bsearch to find the functions that know how to process the memory block.
The memory for the g_stDVInfo array is allocated out of a private heap that I set up just for the library. As discussed above, the MemDumperValidator library uses the #pragma init_seg directive to get the C++ static class, AutoMatic, constructed before your code and destructed after your code. The constructor for the AutoMatic class just calls the InitializeLibrary function to get the file static variables set up and the private heap allocated.
Now that all the framework stuff is out of the way, I can turn to the code that uses the debug runtime. The last step in the InitializeLibrary function is a call to _CrtSetDumpClient, which sets DumpFunction (a very original name) as the client dump function. When DumpFunction is called by the library, it uses the FindRegisteredBlockType helper function to see if the block has a registered dump function. If there is one for the block, it is called to dump the block. If there is not a registered dump function, and if there is a previous client dump function, then that client dump function is called. Finally, if there was no registered dump function and no previous client dump function, then I just dump the block the same way the debug runtime does it.
The ValidateAllBlocks function uses two features of the debug runtime. First it calls _CrtCheckMemory to allow it to check for overwrites and underwrites. It then calls _CrtDoForAllClientObjects, which will loop through all the allocated client blocks and call the callback function passed as a parameter. My DoForAllFunction does much the same thing as DumpFunction in looking to see if the block type is registered; if so, it calls the validation function for the block.
When I first got the MemDumperValidator library running, I was pretty happy how it all worked—except that, when the program terminated, I never saw the nicely formatted output from my dumping functions if I had memory leaks. The memory dumps were just the old standard debug runtime-style dumps. I tracked this down and was surprised to see that the runtime termination routines call _CrtSetDumpClient with a parameter of NULL, thus clearing out my dump hook before calling _CrtDumpMemoryLeaks. This was a little distressing, until it dawned on me that I just had to do the final memory leak checking myself. Fortunately, I had the perfect place to do it. 
Since I was already using the #pragma init_seg(compiler) to get the AutoMatic class initialized before your code and to call the destructor after your code, I just needed to do the leak checking there, then turn off the _CRTDBG_LEAK_ CHECK_DF flag so that the debug runtime does not do it. The only caveat is that you need to make sure that the runtime library of your choice comes before MemDumperValidator.lib if you link with the NODEFAULTLIB switch. This is needed so that you get everything created and destroyed in the correct order.
If you think about it, it makes sense that the runtime clears out any dump hook installed. If your dump hook were using any runtime routines, such as printf, it could crash the termination of your program because the runtime is in the middle of shutting down when it calls _CrtDumpMemoryLeaks. If you follow the rules and always link with the runtime library before any others, you will be fine because the MemDumperValidator library is shut down before the runtime. To avoid problems, only use the _RPTn macros in your dumper routines anyway, since this is all that _CrtDumpMemoryLeaks uses.
The Memory Stress Library
Now that you can dump and validate all of your memory blocks in a clean and usable fashion, it is time to put some stress in your life. Back in the old 16-bit Windows days, the neatest program that came with the SDK was STRESS.EXE. It allowed you to do all sorts of nasty things like eat up disk space, gobble up the GDI heap, and use up the file handles. It even had a neat icon of an elephant walking a tightrope. 
While it is much harder to stress Win32 programs, the debug runtime allows you to hook into the allocation system and let it succeed or fail. While I will leave it up to you to write the disk-eating code, MemStressLib will give you a means to stress your C and C++ memory allocation (see Figure 6). To make it really easy to use, I even wrote a Visual Basic front end, MemStress, so you can specify exactly what conditions you would like to fail.
The memory stress library lets you force allocation failures based on various criteria: all allocations, on every n allocations, after x bytes are allocated, on requests over y bytes, on all allocations out of a file, and on a specific line in a file. In addition, you can have MemStressLib prompt you on each allocation, and you can also set the debug runtime flags you would like in effect for your program. The MemStressDemo program is a sample MFC program that allows you to experiment with setting different options from the MemStress UI and seeing the results.
Using MemStressLib is relatively simple. In your code, include MemStressLib.h and call the INITMEMSTRESS macro with the name of your program. To stop the memory allocation hooking, use the SHUTDOWNMEMSTRESS macro. You can start and stop the hook as many times as you like when running your program.
When you have compiled your program, start the MemStress user interface, click the Add Program button, and type the same name that you specified in the INITMEMSTRESS macro. After you have selected the failure options that you want, press the Save Settings For This Program button to save the settings into MEMSTRESS.INI. Now you can run your program and see how it behaves when it fails memory allocations.
You will probably want to be very selective about using MemStressLib. If, for example, you specify that you want all allocations over 100 bytes to fail, and you have the INITMEMSTRESS call in your MFC application's InitInstance function, you will probably take down MFC because it will be unable to initialize. It is best to limit the use of MemStressLib to key areas in your code so you can test them in isolation.
The vast majority of the implementation of MemStressLib is in the reading and processing of the MEMSTRESS.INI file, where all the settings for individual programs are stored. From the debug runtime perspective, the important function is the call to _CrtSetAllocHook during the MemStressLib initialization as it gets the hook function, AllocationHook, set as the allocation hook. If the allocation hook function returns TRUE, then the allocation request is allowed to continue. By returning FALSE, the allocation hook can have the debug runtime fail the allocation request. The allocation hook only has one hard requirement from the debug runtime: if the type of block, as specified by the nBlockUse parameter, is marked as a _CRT_BLOCK, the hook function must return TRUE to allow the allocation to take place.
The allocation hook gets called on every type of allocation function. The different types, as specified in the first parameter to the hook, are: _HOOK_ALLOC, _HOOK_REALLOC, or _HOOK_FREE. In my AllocationHook function, if the type is _HOOK_FREE I skip all the code that determines if the memory request should pass or fail. For _HOOK_ALLOC and _HOOK_REALLOC types, my AllocationHook function does a series of if statements to determine whether any of the failure conditions are met. If one is met, I set up to return FALSE.
In the AllocationHook __finally block, I track all the statistics for the allocations. The only interesting part is when processing a _HOOK_FREE type allocation, I need to do undocumented work to get at the _CrtMemBlockHeader structure. The debug runtime always calls the hook function for _HOOK_FREE types with a size value of zero, which makes it a little difficult to check for high-water-mark failures. Also, since the actual allocation hook can come and go during a run of the program, I have to make sure that I do not accidentally get into negative amounts of allocated memory when processing the _HOOK_FREE type.
When I looked back on the code for this column, I was struck by how little of it really dealt with the debug runtime and how much of it did the setup work for MemDumperValidator and MemStressLib. I hope you find the libraries useful and they help you find bugs! I encourage you to look for other libraries that you could develop with the debug runtime.
Slay Those Bugs!
In my future column, I want to offer some concise tips that you can use to fight bugs in your code or speed up your debugging. If you have any tips, I would love to hear about them. I will be glad to acknowledge your contribution; just think, you will be able to impress your friends by searching for your name on the MSDN CD! Now, on to my inaugural tips:
Tip 1: Get rid of the MSDEV splash screen when using Just In Time debugging. First, run REGEDIT and open the key HKEY_LOCAL_MACHINE\Software\Microsoft\ Windows NT\CurrentVersion\AeDebug. Then, change the Debugger key from "<your path>\msdev.exe -p %ld -e %ld" to "<your path>\msdev.exe -nologo -p %ld -e %ld."
Tip 2: Always check CloseHandle's return value. Even though there is not much you can do when CloseHandle returns FALSE, it is generally indicative of a serious problem. Since Win32 will reuse handle values, it is quite easy to accidentally close an open handle being used elsewhere in your code, causing weird random bugs. When you call CloseHandle on a handle that has already been closed, it can sometimes cause random crashes that are impossible to track down. Place all your calls to CloseHandle in VERIFY macros so that you stand a fighting chance of finding those bugs in _DEBUG builds. (Thanks to Jim Austin.)

Introducing the Bugslayer: Annihilating Bugs in an Application Near You相关推荐

  1. react引入多个图片_重新引入React:v16之后的每个React更新都已揭开神秘面纱。

    react引入多个图片 In this article (and accompanying book), unlike any you may have come across before, I w ...

  2. UNREAL ENGINE 4.13 正式发布!

    这次的版本带来了数百个虚幻引擎 4 的更新,包括来自 GitHub 的社区成员们提交的 145 个改进!感谢所有为虚幻引擎 4 添砖加瓦贡献的人们: alk3ovation, Allegorithmi ...

  3. TCP/IP第一卷读书笔记

    <!-- /* Font Definitions */ @font-face {font-family:Wingdings; panose-1:5 0 0 0 0 0 0 0 0 0; mso- ...

  4. SEAM IN ACTION有空就翻翻

    说实话,关于SEAM的书不多. 在网上搜seam in action中文版,有,在csdn,花3分下下来,假的.是中文的参考文档(Seam reference).找骂. 自己也真想细致地了解一下SEA ...

  5. 深入了解gradle和maven的区别

    文章目录 简介 gradle和maven的比较 可扩展性 性能比较 依赖的区别 从maven迁移到gradle 自动转换 转换依赖 转换repositories仓库 控制依赖的版本 多模块项目 pro ...

  6. CString Management (关于CString的所有操作)

    CString Management (关于CString的所有操作) 作者:FreeEIM CStrings are a useful data type. They greatly simplif ...

  7. vulcan 编程_如何用Next代替流星— Vulcan Next Starter简介

    vulcan 编程 2020年,仍在寻找高效的JS框架 (2020, still looking for a productive JS framework) When you create a pr ...

  8. TiDB at ZaloPay Infrastructure Lesson Learned

    作者:gingerkidney 原文来源: https://tidb.net/blog/7207e46f Industry: Mobile Payment Author: Tan To Nguyen ...

  9. SitePoint播客#38:猫的大脑

    Episode 38 of The SitePoint Podcast is now available! This week your hosts are Stephan Segraves (@ss ...

最新文章

  1. 吃惊!江苏抽查发现,144篇硕士学位论文不合格,部分单位将被约谈
  2. SpringCloud使用Sofa-lookout监控(基于Eureka)
  3. 正确解读free -m
  4. 解决: Sudamod/CM-13.0 源代码出现 Fatal: duplicate project .....问题
  5. spring(4)面向切面的Spring(AOP)
  6. [js] 一道变态题 Number.call.call(Number, undefined, 0) 等于什么?
  7. 实用的编程网站—良好的开端
  8. mysql join图解_MySQL中Join算法实现原理分析[多图]
  9. 端口隔离配置命令、端口镜像(抓包配置)详解(附图,建议PC观看)
  10. mysql中的where和having子句的区别
  11. 批量替换_【脚本】AE照片墙模板图片批量替换脚本Multi Replacer
  12. Oracle 有long类型字段的表 使用insert into select 语句 ,出现:ORA-00997 错误
  13. revit二次开发 材质类别分析
  14. chrome打不开网页 转圈圈
  15. 计算机键盘上的句号键在哪,电脑键盘句号是哪个键
  16. 导数卷积 牛客 NTT
  17. 百度海洋引擎Ocean Engine,打破“数据孤岛”的新利器
  18. 实验二、贪吃蛇的游戏开发
  19. 特征工程——推荐系统里的特征工程
  20. java自动化测试语言高级之MySQL 连接

热门文章

  1. 你是否常常下决心“不改变”?
  2. 【虹科直播回顾】笔记及问题解答 | AR解决方案助力数字工厂降本增效
  3. iis 安装护卫神出现乱码,解决:鏃犳硶鏄剧ず椤甸潰锛屽洜涓哄彂鐢熷唴閮ㄦ湇鍔″櫒閿欒銆�
  4. 云云联邦cross-silo场景中如何修改服务侧聚合算法
  5. 价值50元的图文很详细的装机教程
  6. php htmlspecial 安全,php对输入的安全性处理函数trim、stripslashes、htmlspecialchars
  7. Java基础知识回顾之三 ----- 封装、继承和多态
  8. 如何在微信号限制加群后实现精准吸粉?
  9. CKEditor4解决首行缩进问题
  10. Javascript点击事件的3种写法(复习)