* Copyright 2012-2016, Rene Gollent, rene@gollent.com.
* Distributed under the terms of the MIT License.
*/
#include "DebugReportGenerator.h"
#include <cpu_type.h>
#include <AutoLocker.h>
#include <Entry.h>
#include <File.h>
#include <Path.h>
#include <StringForSize.h>
#include "Architecture.h"
#include "AreaInfo.h"
#include "AutoDeleter.h"
#include "CpuState.h"
#include "DebuggerInterface.h"
#include "DisassembledCode.h"
#include "FunctionInstance.h"
#include "Image.h"
#include "ImageDebugInfo.h"
#include "MessageCodes.h"
#include "Register.h"
#include "SemaphoreInfo.h"
#include "StackFrame.h"
#include "StackTrace.h"
#include "Statement.h"
#include "SystemInfo.h"
#include "Team.h"
#include "TeamDebugInfo.h"
#include "Thread.h"
#include "Type.h"
#include "UiUtils.h"
#include "UserInterface.h"
#include "Value.h"
#include "ValueLoader.h"
#include "ValueLocation.h"
#include "ValueNode.h"
#include "ValueNodeManager.h"
#define WRITE_AND_CHECK(output, data) \
{ \
ssize_t error = output.Write(data.String(), data.Length()); \
if (error < 0) \
return error; \
}
DebugReportGenerator::DebugReportGenerator(::Team* team,
UserInterfaceListener* listener, DebuggerInterface* interface)
:
BLooper("DebugReportGenerator"),
fTeam(team),
fArchitecture(team->GetArchitecture()),
fDebuggerInterface(interface),
fTeamDataSem(-1),
fNodeManager(NULL),
fListener(listener),
fWaitingNode(NULL),
fCurrentBlock(NULL),
fBlockRetrievalStatus(B_OK),
fTraceWaitingThread(NULL),
fSourceWaitingFunction(NULL)
{
fTeam->AddListener(this);
fArchitecture->AcquireReference();
}
DebugReportGenerator::~DebugReportGenerator()
{
fTeam->RemoveListener(this);
fArchitecture->ReleaseReference();
if (fNodeManager != NULL) {
fNodeManager->RemoveListener(this);
fNodeManager->ReleaseReference();
}
if (fCurrentBlock != NULL)
fCurrentBlock->ReleaseReference();
if (fTeamDataSem >= 0)
delete_sem(fTeamDataSem);
}
status_t
DebugReportGenerator::Init()
{
fTeamDataSem = create_sem(0, "debug_controller_data_wait");
if (fTeamDataSem < B_OK)
return fTeamDataSem;
fNodeManager = new(std::nothrow) ValueNodeManager();
if (fNodeManager == NULL)
return B_NO_MEMORY;
fNodeManager->AddListener(this);
Run();
return B_OK;
}
DebugReportGenerator*
DebugReportGenerator::Create(::Team* team, UserInterfaceListener* listener,
DebuggerInterface* interface)
{
DebugReportGenerator* self = new DebugReportGenerator(team, listener,
interface);
try {
self->Init();
} catch (...) {
delete self;
throw;
}
return self;
}
status_t
DebugReportGenerator::_GenerateReport(const char* outputPath)
{
BFile file(outputPath, B_WRITE_ONLY | B_CREATE_FILE | B_ERASE_FILE);
status_t result = file.InitCheck();
if (result != B_OK)
return result;
result = _GenerateReportHeader(file);
if (result != B_OK)
return result;
result = _DumpRunningThreads(file);
if (result != B_OK)
return result;
result = _DumpLoadedImages(file);
if (result != B_OK)
return result;
result = _DumpAreas(file);
if (result != B_OK)
return result;
result = _DumpSemaphores(file);
if (result != B_OK)
return result;
AutoLocker< ::Team> teamLocker(fTeam);
fTeam->NotifyDebugReportChanged(outputPath, B_OK);
return B_OK;
}
void
DebugReportGenerator::MessageReceived(BMessage* message)
{
switch (message->what) {
case MSG_GENERATE_DEBUG_REPORT:
{
entry_ref ref;
if (message->FindRef("target", &ref) == B_OK) {
BPath path(&ref);
status_t error = _GenerateReport(path.Path());
if (error != B_OK)
fTeam->NotifyDebugReportChanged(path.Path(), error);
}
break;
}
default:
BLooper::MessageReceived(message);
break;
}
}
void
DebugReportGenerator::ThreadStackTraceChanged(const ::Team::ThreadEvent& event)
{
if (fTraceWaitingThread == event.GetThread()) {
fTraceWaitingThread = NULL;
release_sem(fTeamDataSem);
}
}
void
DebugReportGenerator::MemoryBlockRetrieved(TeamMemoryBlock* block)
{
_HandleMemoryBlockRetrieved(block, B_OK);
}
void
DebugReportGenerator::MemoryBlockRetrievalFailed(TeamMemoryBlock* block,
status_t result)
{
_HandleMemoryBlockRetrieved(block, result);
}
void
DebugReportGenerator::ValueNodeValueChanged(ValueNode* node)
{
if (node == fWaitingNode) {
fWaitingNode = NULL;
release_sem(fTeamDataSem);
}
}
void
DebugReportGenerator::FunctionSourceCodeChanged(Function* function)
{
AutoLocker< ::Team> teamLocker(fTeam);
if (function == fSourceWaitingFunction) {
function_source_state state = function->SourceCodeState();
switch (state) {
case FUNCTION_SOURCE_LOADED:
case FUNCTION_SOURCE_SUPPRESSED:
case FUNCTION_SOURCE_UNAVAILABLE:
{
fSourceWaitingFunction->RemoveListener(this);
fSourceWaitingFunction = NULL;
release_sem(fTeamDataSem);
}
default:
break;
}
}
}
status_t
DebugReportGenerator::_GenerateReportHeader(BFile& _output)
{
AutoLocker< ::Team> locker(fTeam);
BString data;
data.SetToFormat("Debug information for team %s (%" B_PRId32 "):\n",
fTeam->Name(), fTeam->ID());
WRITE_AND_CHECK(_output, data);
cpu_platform platform = B_CPU_UNKNOWN;
cpu_vendor cpuVendor = B_CPU_VENDOR_UNKNOWN;
uint32 cpuModel = 0;
uint32 topologyNodeCount = 0;
cpu_topology_node_info* topology = NULL;
get_cpu_topology_info(NULL, &topologyNodeCount);
if (topologyNodeCount != 0) {
topology = new(std::nothrow) cpu_topology_node_info[topologyNodeCount];
if (topology == NULL)
return B_NO_MEMORY;
BPrivate::ArrayDeleter<cpu_topology_node_info> deleter(topology);
get_cpu_topology_info(topology, &topologyNodeCount);
for (uint32 i = 0; i < topologyNodeCount; i++) {
switch (topology[i].type) {
case B_TOPOLOGY_ROOT:
platform = topology[i].data.root.platform;
break;
case B_TOPOLOGY_PACKAGE:
cpuVendor = topology[i].data.package.vendor;
break;
case B_TOPOLOGY_CORE:
cpuModel = topology[i].data.core.model;
break;
default:
break;
}
}
}
SystemInfo sysInfo;
if (fDebuggerInterface->GetSystemInfo(sysInfo) == B_OK) {
const system_info &info = sysInfo.GetSystemInfo();
const char* vendor = get_cpu_vendor_string(cpuVendor);
const char* model = get_cpu_model_string(platform, cpuVendor, cpuModel);
data.SetToFormat("CPU(s): %" B_PRId32 "x %s %s\n",
info.cpu_count, vendor != NULL ? vendor : "unknown",
model != NULL ? model : "unknown");
WRITE_AND_CHECK(_output, data);
char maxSize[32];
char usedSize[32];
data.SetToFormat("Memory: %s total, %s used\n",
BPrivate::string_for_size((int64)info.max_pages * B_PAGE_SIZE,
maxSize, sizeof(maxSize)),
BPrivate::string_for_size((int64)info.used_pages * B_PAGE_SIZE,
usedSize, sizeof(usedSize)));
WRITE_AND_CHECK(_output, data);
const utsname& name = sysInfo.GetSystemName();
data.SetToFormat("Haiku revision: %s (%s)\n", name.version,
name.machine);
WRITE_AND_CHECK(_output, data);
}
return B_OK;
}
status_t
DebugReportGenerator::_DumpLoadedImages(BFile& _output)
{
AutoLocker< ::Team> locker(fTeam);
BString data("\nLoaded Images:\n");
WRITE_AND_CHECK(_output, data);
BObjectList<Image> images;
for (ImageList::ConstIterator it = fTeam->Images().GetIterator();
Image* image = it.Next();) {
images.AddItem(image);
}
images.SortItems(&_CompareImages);
Image* image = NULL;
data.SetToFormat("\tID\t\tText Base\tText End\tData Base\tData"
" End\tType\tName\n\t");
WRITE_AND_CHECK(_output, data);
data.Truncate(0L);
data.Append('-', 80);
data.Append("\n");
WRITE_AND_CHECK(_output, data);
for (int32 i = 0; (image = images.ItemAt(i)) != NULL; i++) {
const ImageInfo& info = image->Info();
char buffer[32];
try {
target_addr_t textBase = info.TextBase();
target_addr_t dataBase = info.DataBase();
data.SetToFormat("\t%" B_PRId32 "\t0x%08" B_PRIx64 "\t"
"0x%08" B_PRIx64 "\t0x%08" B_PRIx64 "\t0x%08" B_PRIx64 "\t"
"%-7s\t%s\n", info.ImageID(), textBase, textBase + info.TextSize(),
dataBase, dataBase + info.DataSize(),
UiUtils::ImageTypeToString(info.Type(), buffer,
sizeof(buffer)), info.Name().String());
WRITE_AND_CHECK(_output, data);
} catch (...) {
return B_NO_MEMORY;
}
}
return B_OK;
}
status_t
DebugReportGenerator::_DumpAreas(BFile& _output)
{
BObjectList<AreaInfo, true> areas(20);
status_t result = fDebuggerInterface->GetAreaInfos(areas);
if (result != B_OK)
return result;
areas.SortItems(&_CompareAreas);
BString data("\nAreas:\n");
WRITE_AND_CHECK(_output, data);
data.SetToFormat("\tID\t\tBase\t\tEnd\t\t\tSize (KiB)\tProtection\tLocking\t\t\tName\n\t");
WRITE_AND_CHECK(_output, data);
data.Truncate(0L);
data.Append('-', 80);
data.Append("\n");
WRITE_AND_CHECK(_output, data);
AreaInfo* info;
BString protectionBuffer;
char lockingBuffer[32];
for (int32 i = 0; (info = areas.ItemAt(i)) != NULL; i++) {
try {
data.SetToFormat("\t%" B_PRId32 "\t0x%08" B_PRIx64 "\t"
"0x%08" B_PRIx64 "\t%10" B_PRId64 "\t%-11s\t%-14s\t%s\n",
info->AreaID(), info->BaseAddress(), info->BaseAddress()
+ info->Size(), info->Size() / 1024,
UiUtils::AreaProtectionFlagsToString(info->Protection(),
protectionBuffer).String(),
UiUtils::AreaLockingFlagsToString(info->Lock(), lockingBuffer,
sizeof(lockingBuffer)), info->Name().String());
WRITE_AND_CHECK(_output, data);
} catch (...) {
return B_NO_MEMORY;
}
}
data = "\nProtection Flags: r - read, w - write, x - execute, "
"s - stack, o - overcommit, c - cloneable, S - shared, k - kernel\n";
WRITE_AND_CHECK(_output, data);
return B_OK;
}
status_t
DebugReportGenerator::_DumpSemaphores(BFile& _output)
{
BObjectList<SemaphoreInfo, true> semaphores(20);
status_t error = fDebuggerInterface->GetSemaphoreInfos(semaphores);
if (error != B_OK)
return error;
semaphores.SortItems(&_CompareSemaphores);
BString data = "\nSemaphores:\n";
WRITE_AND_CHECK(_output, data);
data.SetToFormat("\tID\t\tCount\tLast Holder\tName\n\t");
WRITE_AND_CHECK(_output, data);
data.Truncate(0L);
data.Append('-', 60);
data.Append("\n");
WRITE_AND_CHECK(_output, data);
SemaphoreInfo* info;
for (int32 i = 0; (info = semaphores.ItemAt(i)) != NULL; i++) {
try {
data.SetToFormat("\t%" B_PRId32 "\t%5" B_PRId32 "\t%11" B_PRId32
"\t%s\n", info->SemID(), info->Count(),
info->LatestHolder(), info->Name().String());
WRITE_AND_CHECK(_output, data);
} catch (...) {
return B_NO_MEMORY;
}
}
return B_OK;
}
status_t
DebugReportGenerator::_DumpRunningThreads(BFile& _output)
{
AutoLocker< ::Team> locker(fTeam);
BString data("\nActive Threads:\n");
WRITE_AND_CHECK(_output, data);
BObjectList< ::Thread> threads;
::Thread* thread;
for (ThreadList::ConstIterator it = fTeam->Threads().GetIterator();
(thread = it.Next());) {
threads.AddItem(thread);
thread->AcquireReference();
}
status_t error = B_OK;
threads.SortItems(&_CompareThreads);
for (int32 i = 0; (thread = threads.ItemAt(i)) != NULL; i++) {
try {
data.SetToFormat("\tthread %" B_PRId32 ": %s %s\n", thread->ID(),
thread->Name(), thread->IsMainThread()
? "(main)" : "");
WRITE_AND_CHECK(_output, data);
if (thread->State() == THREAD_STATE_STOPPED) {
data.SetToFormat("\t\tstate: %s",
UiUtils::ThreadStateToString(thread->State(),
thread->StoppedReason()));
const BString& stoppedInfo = thread->StoppedReasonInfo();
if (stoppedInfo.Length() != 0)
data << " (" << stoppedInfo << ")";
data << "\n\n";
WRITE_AND_CHECK(_output, data);
locker.Unlock();
error = _DumpDebuggedThreadInfo(_output, thread);
locker.Lock();
if (error != B_OK)
break;
}
} catch (...) {
error = B_NO_MEMORY;
}
}
for (int32 i = 0; (thread = threads.ItemAt(i)) != NULL; i++)
thread->ReleaseReference();
return error;
}
status_t
DebugReportGenerator::_DumpDebuggedThreadInfo(BFile& _output,
::Thread* thread)
{
AutoLocker< ::Team> locker;
if (thread->State() != THREAD_STATE_STOPPED)
return B_OK;
status_t error;
StackTrace* trace = NULL;
for (;;) {
trace = thread->GetStackTrace();
if (trace != NULL)
break;
locker.Unlock();
fTraceWaitingThread = thread;
do {
error = acquire_sem(fTeamDataSem);
} while (error == B_INTERRUPTED);
if (error != B_OK)
return error;
locker.Lock();
}
BString data("\t\tFrame\t\tIP\t\t\tFunction Name\n");
WRITE_AND_CHECK(_output, data);
data = "\t\t-----------------------------------------------\n";
WRITE_AND_CHECK(_output, data);
for (int32 i = 0; StackFrame* frame = trace->FrameAt(i); i++) {
char functionName[512];
BString sourcePath;
target_addr_t ip = frame->InstructionPointer();
Image* image = fTeam->ImageByAddress(ip);
FunctionInstance* functionInstance = NULL;
if (image != NULL && image->ImageDebugInfoState()
== IMAGE_DEBUG_INFO_LOADED) {
ImageDebugInfo* info = image->GetImageDebugInfo();
functionInstance = info->FunctionAtAddress(ip);
}
if (functionInstance != NULL) {
Function* function = functionInstance->GetFunction();
if (function->SourceCodeState() == FUNCTION_SOURCE_NOT_LOADED
&& functionInstance->SourceCodeState()
== FUNCTION_SOURCE_NOT_LOADED) {
fSourceWaitingFunction = function;
fSourceWaitingFunction->AddListener(this);
fListener->FunctionSourceCodeRequested(functionInstance);
locker.Unlock();
do {
error = acquire_sem(fTeamDataSem);
} while (error == B_INTERRUPTED);
if (error != B_OK)
return error;
locker.Lock();
}
}
Statement* statement;
if (fTeam->GetStatementAtAddress(ip,
functionInstance, statement) == B_OK) {
BReference<Statement> statementReference(statement, true);
int32 line = statement->StartSourceLocation().Line();
LocatableFile* sourceFile = functionInstance->GetFunction()
->SourceFile();
if (sourceFile != NULL) {
sourceFile->GetPath(sourcePath);
sourcePath.SetToFormat("(%s:%" B_PRId32 ")",
sourcePath.String(), line);
}
}
data.SetToFormat("\t\t%#08" B_PRIx64 "\t%#08" B_PRIx64 "\t%s %s\n",
frame->FrameAddress(), ip, UiUtils::FunctionNameForFrame(
frame, functionName, sizeof(functionName)),
sourcePath.String());
WRITE_AND_CHECK(_output, data);
if (i == 0) {
locker.Unlock();
error = _DumpFunctionDisassembly(_output,
frame->InstructionPointer());
if (error != B_OK)
return error;
error = _DumpStackFrameMemory(_output, thread->GetCpuState(),
frame->FrameAddress(), thread->GetTeam()->GetArchitecture()
->StackGrowthDirection());
if (error != B_OK)
return error;
locker.Lock();
}
if (frame->CountParameters() == 0 && frame->CountLocalVariables() == 0)
continue;
data = "\t\t\tVariables:\n";
WRITE_AND_CHECK(_output, data);
error = fNodeManager->SetStackFrame(thread, frame);
if (error != B_OK)
continue;
ValueNodeContainer* container = fNodeManager->GetContainer();
AutoLocker<ValueNodeContainer> containerLocker(container);
for (int32 i = 0; i < container->CountChildren(); i++) {
data.Truncate(0L);
ValueNodeChild* child = container->ChildAt(i);
containerLocker.Unlock();
_ResolveValueIfNeeded(child->Node(), frame, 1);
containerLocker.Lock();
UiUtils::PrintValueNodeGraph(data, child, 3, 1);
WRITE_AND_CHECK(_output, data);
}
data = "\n";
WRITE_AND_CHECK(_output, data);
}
data = "\n\t\tRegisters:\n";
WRITE_AND_CHECK(_output, data);
CpuState* state = thread->GetCpuState();
BVariant value;
const Register* reg = NULL;
for (int32 i = 0; i < fArchitecture->CountRegisters(); i++) {
reg = fArchitecture->Registers() + i;
state->GetRegisterValue(reg, value);
if (reg->Format() == REGISTER_FORMAT_SIMD) {
data.SetToFormat("\t\t\t%5s:\t%s\n", reg->Name(),
UiUtils::FormatSIMDValue(value, reg->BitSize(),
SIMD_RENDER_FORMAT_INT16, data).String());
} else {
char buffer[64];
data.SetToFormat("\t\t\t%5s:\t%s\n", reg->Name(),
UiUtils::VariantToString(value, buffer, sizeof(buffer)));
}
WRITE_AND_CHECK(_output, data);
}
return B_OK;
}
status_t
DebugReportGenerator::_DumpFunctionDisassembly(BFile& _output,
target_addr_t instructionPointer)
{
AutoLocker< ::Team> teamLocker(fTeam);
BString data;
FunctionInstance* instance = NULL;
Image* image = fTeam->ImageByAddress(instructionPointer);
if (image == NULL) {
data.SetToFormat("\t\t\tUnable to retrieve disassembly for IP %#"
B_PRIx64 ": address not contained in any valid image.\n",
instructionPointer);
WRITE_AND_CHECK(_output, data);
return B_OK;
}
ImageDebugInfo* info = image->GetImageDebugInfo();
if (info == NULL) {
data.SetToFormat("\t\t\tUnable to retrieve disassembly for IP %#"
B_PRIx64 ": no debug info available for image '%s'.\n",
instructionPointer, image->Name().String());
WRITE_AND_CHECK(_output, data);
return B_OK;
}
instance = info->FunctionAtAddress(instructionPointer);
if (instance == NULL) {
data.SetToFormat("\t\t\tUnable to retrieve disassembly for IP %#"
B_PRIx64 ": address does not point to a function.\n",
instructionPointer);
WRITE_AND_CHECK(_output, data);
return B_OK;
}
Statement* statement = NULL;
DisassembledCode* code = instance->GetSourceCode();
BReference<DisassembledCode> codeReference;
if (code == NULL) {
status_t error = fTeam->DebugInfo()->DisassembleFunction(instance,
code);
if (error != B_OK) {
data.SetToFormat("\t\t\tUnable to retrieve disassembly for IP %#"
B_PRIx64 ": %s.\n", instructionPointer, strerror(error));
WRITE_AND_CHECK(_output, data);
return B_OK;
}
codeReference.SetTo(code, true);
} else
codeReference.SetTo(code);
statement = code->StatementAtAddress(instructionPointer);
if (statement == NULL) {
data.SetToFormat("\t\t\tUnable to retrieve disassembly for IP %#"
B_PRIx64 ": address does not map to a valid instruction.\n",
instructionPointer);
WRITE_AND_CHECK(_output, data);
return B_OK;
}
SourceLocation location = statement->StartSourceLocation();
data = "\t\t\tDisassembly:\n";
WRITE_AND_CHECK(_output, data);
for (int32 i = 0; i <= location.Line(); i++) {
data = "\t\t\t\t";
data << code->LineAt(i);
if (i == location.Line())
data << " <--";
data << "\n";
WRITE_AND_CHECK(_output, data);
}
data = "\n";
WRITE_AND_CHECK(_output, data);
return B_OK;
}
status_t
DebugReportGenerator::_DumpStackFrameMemory(BFile& _output,
CpuState* state, target_addr_t framePointer, uint8 stackDirection)
{
target_addr_t startAddress;
target_addr_t endAddress;
if (stackDirection == STACK_GROWTH_DIRECTION_POSITIVE) {
startAddress = framePointer;
endAddress = state->StackPointer();
} else {
startAddress = state->StackPointer();
endAddress = framePointer;
}
if (endAddress <= startAddress)
return B_OK;
if (fCurrentBlock == NULL || !fCurrentBlock->Contains(startAddress)) {
status_t error;
fListener->InspectRequested(startAddress, this);
do {
error = acquire_sem(fTeamDataSem);
} while (error == B_INTERRUPTED);
if (error != B_OK)
return error;
}
BString data("\t\t\tFrame memory:\n");
WRITE_AND_CHECK(_output, data);
if (fBlockRetrievalStatus == B_OK) {
data.Truncate(0L);
UiUtils::DumpMemory(data, 4, fCurrentBlock, startAddress, 1, 16,
endAddress - startAddress);
WRITE_AND_CHECK(_output, data);
} else {
data.SetToFormat("\t\t\t\tUnavailable (%s)\n", strerror(
fBlockRetrievalStatus));
WRITE_AND_CHECK(_output, data);
}
return B_OK;
}
status_t
DebugReportGenerator::_ResolveValueIfNeeded(ValueNode* node, StackFrame* frame,
int32 maxDepth)
{
status_t result = B_OK;
if (node->LocationAndValueResolutionState() == VALUE_NODE_UNRESOLVED) {
fWaitingNode = node;
fListener->ValueNodeValueRequested(frame->GetCpuState(),
fNodeManager->GetContainer(), node);
do {
result = acquire_sem(fTeamDataSem);
} while (result == B_INTERRUPTED);
}
if (node->LocationAndValueResolutionState() == B_OK && maxDepth > 0) {
AutoLocker<ValueNodeContainer> containerLocker(
fNodeManager->GetContainer());
for (int32 i = 0; i < node->CountChildren(); i++) {
ValueNodeChild* child = node->ChildAt(i);
containerLocker.Unlock();
result = fNodeManager->AddChildNodes(child);
if (result != B_OK)
continue;
if (node->GetType()->ResolveRawType(false)->Kind() == TYPE_ADDRESS
&& child->GetType()->ResolveRawType(false)->Kind()
== TYPE_COMPOUND) {
_ResolveValueIfNeeded(child->Node(), frame, maxDepth);
} else
_ResolveValueIfNeeded(child->Node(), frame, maxDepth - 1);
containerLocker.Lock();
}
}
return result;
}
void
DebugReportGenerator::_HandleMemoryBlockRetrieved(TeamMemoryBlock* block,
status_t result)
{
if (fCurrentBlock != NULL) {
fCurrentBlock->ReleaseReference();
fCurrentBlock = NULL;
}
fBlockRetrievalStatus = result;
fCurrentBlock = block;
release_sem(fTeamDataSem);
}
int
DebugReportGenerator::_CompareAreas(const AreaInfo* a, const AreaInfo* b)
{
if (a->BaseAddress() < b->BaseAddress())
return -1;
return 1;
}
int
DebugReportGenerator::_CompareImages(const Image* a, const Image* b)
{
if (a->Info().TextBase() < b->Info().TextBase())
return -1;
return 1;
}
int
DebugReportGenerator::_CompareSemaphores(const SemaphoreInfo* a,
const SemaphoreInfo* b)
{
if (a->SemID() < b->SemID())
return -1;
return 1;
}
int
DebugReportGenerator::_CompareThreads(const ::Thread* a,
const ::Thread* b)
{
if (a->State() == b->State())
return a->ID() < b->ID() ? -1 : 1;
if (a->State() == THREAD_STATE_STOPPED && b->State()
!= THREAD_STATE_STOPPED) {
return 1;
}
return -1;
}