├── breakpoint.h ├── README.md ├── LICENSE ├── test └── test.cpp └── breakpoint.cpp /breakpoint.h: -------------------------------------------------------------------------------- 1 | #pragma once 2 | 3 | namespace HWBreakpoint 4 | { 5 | enum class Condition 6 | { 7 | Write = 1, 8 | ReadWrite = 3 9 | }; 10 | 11 | bool Set(void* address, Condition when); 12 | void Clear(void* address); 13 | void ClearAll(); 14 | }; 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # hw-breakpoint 2 | set hardware breakpoints programmaticaly - for windows x86/x64 3 | 4 | ## Features 5 | 6 | * Works both on x86 and x64 7 | * Multithreaded: ability to break every existed thread (at BP registration time), and future thread 8 | 9 | ## Usage 10 | ```c++ 11 | #include "breakpoint.h" 12 | 13 | // let's assume you have variable "val" that you want to watch on it 14 | SomeNativeType val; 15 | 16 | // to set write breakpoint 17 | HWBreakpoint::Set(&val, HWBreakpoint::Condition::Write); 18 | 19 | // to set read and write breakpoint 20 | HWBreakpoint::Set(&val, HWBreakpoint::Condition::ReadWrite); 21 | 22 | // to clear the breakpoint 23 | HWBreakpoint::Clear(&val); 24 | 25 | // to cleanup all breakpoint 26 | HWBreakpoint::ClearAll(); 27 | ``` 28 | ## Credit 29 | The accessing debug register code is based on https://github.com/mmorearty/hardware-breakpoints 30 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 mmeshi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /test/test.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | #include 4 | #include "..\breakpoint.h" 5 | 6 | // extending console output with colors 7 | enum ConsoleColor 8 | { 9 | White = 7, 10 | Green = 10, 11 | Red = 12 12 | }; 13 | std::ostream& operator<<(std::ostream& os, ConsoleColor color) 14 | { 15 | static HANDLE hConsole = ::GetStdHandle(STD_OUTPUT_HANDLE); 16 | ::SetConsoleTextAttribute(hConsole, color); 17 | return os; 18 | }; 19 | 20 | // globals 21 | int g_val = 0; 22 | HANDLE g_hEvent1, g_hEvent2; 23 | 24 | enum TestType 25 | { 26 | Write, 27 | Read 28 | }; 29 | 30 | auto defTryWriteFunc = []() { std::cout << "\tmissed writes " << Red << "[failed]" << White << std::endl; }; 31 | auto defExceptWriteFunc = []() { std::cout << "\tcatch write attempt " << Green << "[ok]" << White << std::endl; }; 32 | auto defTryReadFunc = []() { std::cout << "\tmissed read " << Red << "[failed]" << White << std::endl; }; 33 | auto defExceptReadFunc = []() { std::cout << "\tcatch read attempt " << Green << "[ok]" << White << std::endl; }; 34 | 35 | void tryTest(TestType testType, std::function& tryFunc, std::function& exceptFunc) 36 | { 37 | __try 38 | { 39 | switch (testType) 40 | { 41 | case Write: 42 | std::cout << White << "thread " << std::hex << ::GetCurrentThreadId() << " trying to write..."; 43 | g_val = 1; 44 | break; 45 | case Read: 46 | std::cout << White << "thread " << std::hex << ::GetCurrentThreadId() << " trying to read..."; 47 | volatile int read = g_val; 48 | break; 49 | } 50 | tryFunc(); 51 | } 52 | __except (EXCEPTION_EXECUTE_HANDLER) 53 | { 54 | exceptFunc(); 55 | } 56 | } 57 | 58 | void test(TestType testType, std::function tryFunc = std::function(), std::function exceptFunc = std::function()) 59 | { 60 | if (!tryFunc) 61 | { 62 | switch (testType) 63 | { 64 | case Write: tryFunc = defTryWriteFunc; break; 65 | case Read: tryFunc = defTryReadFunc; break; 66 | default: return; 67 | } 68 | } 69 | 70 | if (!exceptFunc) 71 | { 72 | switch (testType) 73 | { 74 | case Write: exceptFunc = defExceptWriteFunc; break; 75 | case Read: exceptFunc = defExceptReadFunc; break; 76 | default: return; 77 | } 78 | } 79 | 80 | tryTest(testType, tryFunc, exceptFunc); 81 | }; 82 | 83 | DWORD WINAPI ThreadFunc(TestType param) 84 | { 85 | // inform main thread that this thread was created 86 | ::SetEvent(g_hEvent2); 87 | 88 | // wait for main thread signal to continue execution 89 | ::WaitForSingleObject(g_hEvent1, INFINITE); 90 | 91 | test(param); 92 | 93 | return 0; 94 | } 95 | 96 | void runAllTests() 97 | { 98 | // test write 99 | std::cout << "\n\ntest 1: testing write only BP"; 100 | std::cout << "\n=============================\n"; 101 | HWBreakpoint::Set(&g_val, HWBreakpoint::Condition::Write); 102 | test(TestType::Write); 103 | test(TestType::Read, 104 | []() { std::cout << "\tmissed read " << Green << "[ok]" << White << std::endl; }, 105 | []() { std::cout << "\tcatch read attempt " << Red << "[failed]" << White << std::endl; 106 | }); 107 | 108 | // test read & write 109 | std::cout << "\n\ntest 2: testing read & write BP"; 110 | std::cout << "\n===============================\n"; 111 | HWBreakpoint::Set(&g_val, HWBreakpoint::Condition::ReadWrite); 112 | test(TestType::Write); 113 | test(TestType::Read); 114 | 115 | // test clear 116 | std::cout << "\n\ntest 3: clearing BP"; 117 | std::cout << "\n===================\n"; 118 | HWBreakpoint::Clear(&g_val); 119 | test(TestType::Write, 120 | []() { std::cout << "\tmissed write " << Green << "[ok]" << White << std::endl; }, 121 | []() { std::cout << "\tcatch write attempt " << Red << "[failed]" << White << std::endl; 122 | }); 123 | 124 | // multi-thread testing: 125 | HANDLE hTrd; 126 | DWORD threadId; 127 | 128 | g_hEvent1 = ::CreateEvent(NULL, TRUE, FALSE, NULL); 129 | g_hEvent2 = ::CreateEvent(NULL, TRUE, FALSE, NULL); 130 | 131 | std::cout << "\n\ntest 4: existing thread before the BP has setting"; 132 | std::cout << "\n=================================================" << std::endl; 133 | hTrd = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFunc, (LPVOID)TestType::Write, 0, &threadId); 134 | 135 | // wait for new thread creation 136 | ::WaitForSingleObject(g_hEvent2, INFINITE); 137 | 138 | // print out the new thread id 139 | std::cout << "thread " << std::hex << threadId << " has created" << std::endl; 140 | 141 | // set the BP 142 | HWBreakpoint::Set(&g_val, HWBreakpoint::Condition::Write); 143 | 144 | // signal the thread to continue exection (try to write) 145 | ::SetEvent(g_hEvent1); 146 | 147 | // wait for thread completion 148 | ::WaitForSingleObject(hTrd, INFINITE); 149 | 150 | // cleanup and reset events 151 | ::CloseHandle(hTrd); 152 | ::ResetEvent(g_hEvent1); 153 | ::ResetEvent(g_hEvent2); 154 | 155 | std::cout << "\n\ntest 5: new thread after setting the BP"; 156 | std::cout << "\n=======================================" << std::endl; 157 | hTrd = ::CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)ThreadFunc, (LPVOID)TestType::Write, 0, &threadId); 158 | 159 | // wait for new thread creation 160 | ::WaitForSingleObject(g_hEvent2, INFINITE); 161 | 162 | // print out the new thread id 163 | std::cout << "thread " << std::hex << threadId << " has created" << std::endl; 164 | 165 | // signal the thread to continue execution 166 | ::SetEvent(g_hEvent1); 167 | 168 | // wait for thread completion 169 | ::WaitForSingleObject(hTrd, INFINITE); 170 | ::CloseHandle(hTrd); 171 | ::CloseHandle(g_hEvent1); 172 | ::CloseHandle(g_hEvent2); 173 | 174 | HWBreakpoint::ClearAll(); 175 | } 176 | 177 | int main() 178 | { 179 | runAllTests(); 180 | 181 | // wait for user input 182 | std::cin.ignore(); 183 | } 184 | -------------------------------------------------------------------------------- /breakpoint.cpp: -------------------------------------------------------------------------------- 1 | #include "breakpoint.h" 2 | 3 | #include 4 | #include 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | #include 12 | 13 | namespace HWBreakpoint 14 | { 15 | namespace 16 | { 17 | bool _initialize = false; 18 | int _countActive; 19 | void* _address[4]; 20 | int _len[4]; 21 | Condition _when[4]; 22 | 23 | std::thread _workerThread; 24 | std::mutex _mutex, _controlMutex; 25 | std::condition_variable _workerSignal; 26 | std::atomic _workerStop; 27 | 28 | volatile DWORD _pendingThread; 29 | 30 | // hook trampoline 31 | unsigned char* _trampoline; 32 | unsigned char _orgOpcode[8]; 33 | } 34 | 35 | inline void SetBits(ULONG_PTR& dw, int lowBit, int bits, int newValue) 36 | { 37 | int mask = (1 << bits) - 1; // e.g. 1 becomes 0001, 2 becomes 0011, 3 becomes 0111 38 | 39 | dw = (dw & ~(mask << lowBit)) | (newValue << lowBit); 40 | } 41 | void Init(); 42 | void UnInit(); 43 | void BuildTrampoline(); 44 | void ThreadDeutor(); 45 | void SetForThreads(std::unique_lock& lock); 46 | void RegisterThread(DWORD tid); 47 | void ToggleThreadHook(bool set); 48 | void WorkerThreadProc(); 49 | 50 | 51 | // Interface functions 52 | 53 | bool Set(void* address, Condition when) 54 | { 55 | std::lock_guard lock1(_controlMutex); 56 | if (!_initialize) 57 | Init(); 58 | 59 | std::unique_lock lock2(_mutex); 60 | 61 | int index = -1; 62 | 63 | // search for this address 64 | for (int i = 0; i < 4; ++i) 65 | { 66 | if (_address[i] == address) 67 | index = i; 68 | } 69 | 70 | // find avalible place 71 | for (int i = 0; index < 0 && i < 4; ++i) 72 | { 73 | if (_address[i] == nullptr) 74 | { 75 | index = i; 76 | if (_countActive++ == 0) 77 | ToggleThreadHook(true); 78 | } 79 | } 80 | 81 | if (index >= 0) 82 | { 83 | _address[index] = address; 84 | _len[index] = sizeof(void*); 85 | _when[index] = when; 86 | SetForThreads(lock2); 87 | return true; 88 | } 89 | 90 | return false; 91 | } 92 | 93 | void Clear(void* address) 94 | { 95 | std::lock_guard lock1(_controlMutex); 96 | if (!_initialize) 97 | return; 98 | 99 | std::unique_lock lock2(_mutex); 100 | for (int index = 0; index < 4; ++index) 101 | { 102 | if (_address[index] == address) 103 | { 104 | _address[index] = nullptr; 105 | if (--_countActive == 0) 106 | ToggleThreadHook(false); 107 | SetForThreads(lock2); 108 | } 109 | } 110 | } 111 | 112 | void ClearAll() 113 | { 114 | std::lock_guard lock(_controlMutex); 115 | if (!_initialize) 116 | return; 117 | 118 | UnInit(); 119 | } 120 | 121 | // Internal functions 122 | 123 | void Init() 124 | { 125 | if (_initialize) 126 | return; 127 | 128 | std::memset(_address, 0, sizeof(_address)); 129 | _countActive = 0; 130 | 131 | BuildTrampoline(); 132 | if (!_trampoline) 133 | { 134 | std::cout << "[HWBreakpoint] error: failed to build hook function" << std::endl; 135 | return; 136 | } 137 | 138 | _workerStop = true; 139 | _workerThread = std::thread(WorkerThreadProc); 140 | std::unique_lock lock(_mutex); 141 | _workerSignal.wait(lock, []{ return !_workerStop; }); 142 | 143 | _initialize = true; 144 | } 145 | 146 | void UnInit() 147 | { 148 | if (!_initialize) 149 | return; 150 | 151 | ToggleThreadHook(false); 152 | _workerStop = true; 153 | _workerSignal.notify_one(); 154 | _workerThread.join(); 155 | 156 | if (_trampoline) 157 | VirtualFree(_trampoline, 0, MEM_RELEASE); 158 | 159 | _initialize = false; 160 | } 161 | 162 | void BuildTrampoline() 163 | { 164 | ULONG_PTR* rtlThreadStartAddress = (ULONG_PTR*)GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlUserThreadStart"); 165 | 166 | SYSTEM_INFO si; 167 | GetSystemInfo(&si); 168 | 169 | #ifdef _WIN64 170 | 171 | // search for avalible memory in 2GB boundary to host the tampoline function 172 | ULONG_PTR gMinAddress = (ULONG_PTR)si.lpMinimumApplicationAddress; 173 | ULONG_PTR gMaxAddress = (ULONG_PTR)si.lpMaximumApplicationAddress; 174 | ULONG_PTR minAddr = std::max(gMinAddress, (ULONG_PTR)rtlThreadStartAddress - 0x20000000); 175 | ULONG_PTR maxAddr = std::min(gMaxAddress, (ULONG_PTR)rtlThreadStartAddress + 0x20000000); 176 | 177 | const size_t BlockSize = si.dwPageSize; 178 | intptr_t min = minAddr / BlockSize; 179 | intptr_t max = maxAddr / BlockSize; 180 | int rel = 0; 181 | _trampoline = nullptr; 182 | MEMORY_BASIC_INFORMATION mi = { 0 }; 183 | for (int i = 0; i < (max - min + 1); ++i) 184 | { 185 | rel = -rel + (i & 1); 186 | void* pQuery = reinterpret_cast(((min + max) / 2 + rel) * BlockSize); 187 | VirtualQuery(pQuery, &mi, sizeof(mi)); 188 | if (mi.State == MEM_FREE) 189 | { 190 | _trampoline = (unsigned char*)VirtualAlloc(pQuery, BlockSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); 191 | if (_trampoline != nullptr) 192 | break; 193 | } 194 | } 195 | 196 | if (!_trampoline) 197 | return; 198 | 199 | // save prologe hooked function 200 | *(ULONG64*)_orgOpcode = *(ULONG64*)rtlThreadStartAddress; 201 | 202 | *(unsigned char*)&_trampoline[0] = 0x51; // push rcx 203 | *(unsigned char*)&_trampoline[1] = 0x52; // push rdx 204 | *(unsigned char*)&_trampoline[2] = 0x52; // push rdx 205 | *(unsigned short*)&_trampoline[3] = 0x15FF; // call 206 | *(DWORD*)&_trampoline[5] = 0x00000018; // ThreadDeutor 207 | *(unsigned char*)&_trampoline[9] = 0x5A; // pop rdx 208 | *(unsigned char*)&_trampoline[10] = 0x5A; // pop rdx 209 | *(unsigned char*)&_trampoline[11] = 0x59; // pop rcx 210 | 211 | *(DWORD*)&_trampoline[12] = 0x48EC8348; // sub rsp, 0x48 (2 instruction from prologe of target hook) 212 | *(unsigned short*)&_trampoline[16] = 0x8B4C; // mov r9, 213 | *(unsigned char*)&_trampoline[18] = 0xC9; // rcx 214 | *(short*)&_trampoline[19] = 0x25FF; // jmp 215 | *(DWORD*)&_trampoline[21] = 0x00000000; // rtlThreadStartAddress + 7 216 | 217 | // address data for call & jump 218 | *(DWORD64*)&_trampoline[25] = (DWORD64)((unsigned char*)rtlThreadStartAddress + 7); 219 | *(DWORD64*)&_trampoline[33] = (DWORD64)ThreadDeutor; 220 | 221 | #else 222 | 223 | if (((unsigned char*)rtlThreadStartAddress)[0] != 0x89 || ((unsigned char*)rtlThreadStartAddress)[4] != 0x89 || ((unsigned char*)rtlThreadStartAddress)[8] != 0xE9) 224 | return; 225 | 226 | _trampoline = (unsigned char*)VirtualAlloc(NULL, si.dwPageSize, MEM_RESERVE | MEM_COMMIT, PAGE_EXECUTE_READWRITE); 227 | if (!_trampoline) 228 | return; 229 | 230 | // save prologe hooked function 231 | *(ULONG64*)_orgOpcode = *(ULONG64*)rtlThreadStartAddress; 232 | 233 | *(unsigned char*)&_trampoline[0] = 0x50; // push eax 234 | *(unsigned char*)&_trampoline[1] = 0x53; // push ebx 235 | *(unsigned char*)&_trampoline[2] = 0xE8; // call 236 | *(unsigned long*)&_trampoline[3] = (ULONG_PTR)ThreadDeutor - (ULONG_PTR)_trampoline - 7; // ThreadDeutor 237 | *(unsigned char*)&_trampoline[7] = 0x5B; // pop ebx 238 | *(unsigned char*)&_trampoline[8] = 0x58; // pop eax 239 | 240 | // execute 2 instruction from prologe of hooked function 241 | *(unsigned long*)&_trampoline[9] = *rtlThreadStartAddress; 242 | *(unsigned long*)&_trampoline[13] = *(rtlThreadStartAddress + 1); 243 | *(unsigned char*)&_trampoline[17] = 0xE9; // jmp rtlThreadStartAddress + 8 244 | *(unsigned long*)&_trampoline[18] = (ULONG_PTR)rtlThreadStartAddress - (ULONG_PTR)_trampoline - 14; 245 | 246 | 247 | #endif 248 | } 249 | 250 | void ThreadDeutor() 251 | { 252 | std::unique_lock lock(_mutex); 253 | 254 | _pendingThread = GetCurrentThreadId(); 255 | _workerSignal.notify_one(); 256 | _workerSignal.wait(lock, []{ return _pendingThread == -1; }); 257 | } 258 | 259 | void SetForThreads(std::unique_lock& lock) 260 | { 261 | const DWORD pid = GetCurrentProcessId(); 262 | 263 | HANDLE hThreadSnap = INVALID_HANDLE_VALUE; 264 | THREADENTRY32 te32; 265 | hThreadSnap = CreateToolhelp32Snapshot(TH32CS_SNAPTHREAD, 0); 266 | if (hThreadSnap == INVALID_HANDLE_VALUE) 267 | return; 268 | 269 | te32.dwSize = sizeof(THREADENTRY32); 270 | if (!Thread32First(hThreadSnap, &te32)) 271 | { 272 | CloseHandle(hThreadSnap); 273 | return; 274 | } 275 | 276 | do 277 | { 278 | if (te32.th32OwnerProcessID == pid) 279 | { 280 | _pendingThread = te32.th32ThreadID; 281 | _workerSignal.notify_one(); 282 | _workerSignal.wait(lock, [] { return _pendingThread == -1; }); 283 | } 284 | } while (Thread32Next(hThreadSnap, &te32)); 285 | } 286 | 287 | void RegisterThread(DWORD tid) 288 | { 289 | // this function supposed to be called only from worker thread 290 | if (GetCurrentThreadId() == tid) 291 | return; 292 | 293 | HANDLE hThread = OpenThread(THREAD_GET_CONTEXT | THREAD_SET_CONTEXT | THREAD_SUSPEND_RESUME, FALSE, tid); 294 | if (!hThread) 295 | return; 296 | 297 | do 298 | { 299 | CONTEXT cxt; 300 | cxt.ContextFlags = CONTEXT_DEBUG_REGISTERS; 301 | 302 | if (SuspendThread(hThread) == -1) 303 | break; 304 | 305 | if (!GetThreadContext(hThread, &cxt)) 306 | break; 307 | 308 | for (int index = 0; index < 4; ++index) 309 | { 310 | const bool isSet = _address[index] != nullptr; 311 | SetBits(cxt.Dr7, index * 2, 1, isSet); 312 | 313 | if (isSet) 314 | { 315 | switch (index) 316 | { 317 | case 0: cxt.Dr0 = (DWORD_PTR)_address[index]; break; 318 | case 1: cxt.Dr1 = (DWORD_PTR)_address[index]; break; 319 | case 2: cxt.Dr2 = (DWORD_PTR)_address[index]; break; 320 | case 3: cxt.Dr3 = (DWORD_PTR)_address[index]; break; 321 | } 322 | 323 | SetBits(cxt.Dr7, 16 + (index * 4), 2, (int)_when[index]); 324 | SetBits(cxt.Dr7, 18 + (index * 4), 2, (int)_len[index]); 325 | } 326 | } 327 | 328 | if (!SetThreadContext(hThread, &cxt)) 329 | break; 330 | 331 | if (ResumeThread(hThread) == -1) 332 | break; 333 | 334 | std::cout << "[HWBreakpoint] Set/Reset BP for thread: " << std::hex << tid << std::endl; 335 | 336 | } while (false); 337 | 338 | CloseHandle(hThread); 339 | } 340 | 341 | void ToggleThreadHook(bool set) 342 | { 343 | if (!_trampoline) 344 | return; 345 | 346 | //TODO: replacing opcode in system dll might be dangeruos because another thread may invokes and run throw the code while we still replacing it. 347 | // the right solution is to do it from another process which first suspend the target process, inject the changes to his memory, and resume it. 348 | 349 | DWORD oldProtect; 350 | ULONG_PTR* rtlThreadStartAddress = (ULONG_PTR*)GetProcAddress(GetModuleHandle("ntdll.dll"), "RtlUserThreadStart"); 351 | 352 | if (set) 353 | { 354 | VirtualProtect(rtlThreadStartAddress, 5, PAGE_EXECUTE_READWRITE, &oldProtect); 355 | ((unsigned char*)rtlThreadStartAddress)[0] = 0xE9; 356 | *(DWORD*)&(((unsigned char*)rtlThreadStartAddress)[1]) = (DWORD)_trampoline - (DWORD)rtlThreadStartAddress - 5; 357 | 358 | VirtualProtect(rtlThreadStartAddress, 5, oldProtect, &oldProtect); 359 | } 360 | else if (*rtlThreadStartAddress != *(ULONG_PTR*)_orgOpcode) 361 | { 362 | VirtualProtect(rtlThreadStartAddress, 5, PAGE_EXECUTE_READWRITE, &oldProtect); 363 | *(ULONG64*)rtlThreadStartAddress = *(ULONG64*)_orgOpcode; 364 | VirtualProtect(rtlThreadStartAddress, 5, oldProtect, &oldProtect); 365 | } 366 | } 367 | 368 | void WorkerThreadProc() 369 | { 370 | _pendingThread = -1; 371 | _workerStop = false; 372 | _workerSignal.notify_one(); 373 | 374 | while (true) 375 | { 376 | std::unique_lock lock(_mutex); 377 | _workerSignal.wait(lock, [] { return _pendingThread != -1 || _workerStop; }); 378 | if (_workerStop) 379 | return; 380 | 381 | if (_pendingThread != -1) 382 | { 383 | RegisterThread(_pendingThread); 384 | _pendingThread = -1; 385 | _workerSignal.notify_one(); 386 | } 387 | } 388 | } 389 | } 390 | --------------------------------------------------------------------------------