Index: unittests/CMakeLists.txt =================================================================== --- unittests/CMakeLists.txt +++ unittests/CMakeLists.txt @@ -54,6 +54,7 @@ endfunction() add_subdirectory(Breakpoint) +add_subdirectory(tools) add_subdirectory(Core) add_subdirectory(Editline) add_subdirectory(Expression) @@ -73,4 +74,4 @@ if(LLDB_CAN_USE_DEBUGSERVER) add_subdirectory(debugserver) -endif() \ No newline at end of file +endif() Index: unittests/tools/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/CMakeLists.txt @@ -0,0 +1 @@ +add_subdirectory(lldb-server) Index: unittests/tools/lldb-server/.gitignore =================================================================== --- /dev/null +++ unittests/tools/lldb-server/.gitignore @@ -0,0 +1 @@ +thread_inferior Index: unittests/tools/lldb-server/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/lldb-server/CMakeLists.txt @@ -0,0 +1,13 @@ +function(add_lldb_test_executable test_name) + set(EXCLUDE_FROM_ALL ON) + add_llvm_executable(${test_name} NO_INSTALL_RPATH ${ARGN}) + set(outdir ${CMAKE_CURRENT_BINARY_DIR}/${CMAKE_CFG_INTDIR}) + set_output_directory(${test_name} BINARY_DIR ${outdir} LIBRARY_DIR ${outdir}) +endfunction() + +add_lldb_test_executable(thread_inferior inferior/thread_inferior.cpp) + +add_definitions(-DLLDB_SERVER="$") +add_definitions(-DTHREAD_INFERIOR="${CMAKE_CURRENT_BINARY_DIR}/thread_inferior") +add_subdirectory(tests) +add_dependencies(LLDBServerTests thread_inferior) Index: unittests/tools/lldb-server/inferior/thread_inferior.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/inferior/thread_inferior.cpp @@ -0,0 +1,28 @@ +#include +#include +#include +#include + +using namespace std; + +int main(int argc, char* argv[]) +{ + int thread_count = 2; + if (argc > 1) { + thread_count = stoi(argv[1], nullptr, 10); + } + + bool delay = true; + vector threads; + for (int i = 0; i < thread_count; i++) { + threads.push_back(thread([&delay]{while(delay);})); + } + + asm volatile ("int3"); + delay = false; + for (thread& t : threads) { + t.join(); + } + + return 0; +} Index: unittests/tools/lldb-server/tests/CMakeLists.txt =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/CMakeLists.txt @@ -0,0 +1,16 @@ +add_lldb_unittest(LLDBServerTests + TestClient.cpp + TestClientException.cpp + MessageObjects.cpp + ThreadIdsInJstopinfoTest.cpp + + LINK_LIBS + lldbHost + lldbCore + lldbInterpreter + lldbTarget + lldbPluginPlatformLinux + lldbPluginProcessGDBRemote + LINK_COMPONENTS + Support + ) Index: unittests/tools/lldb-server/tests/MessageObjects.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/MessageObjects.h @@ -0,0 +1,88 @@ +#include +#include +#include +#include + +namespace CommunicationTests { + class ThreadInfo; + typedef std::unordered_map ThreadInfoMap; + typedef std::unordered_map ULongMap; + +enum Endian { + LITTLE, + BIG, + UNKNOWN +}; + +class ProcessInfo { +public: + ProcessInfo(const std::string& response); + unsigned int GetPid() const; + Endian GetEndian() const; + +private: + unsigned int pid; + unsigned int parent_pid; + unsigned int real_uid; + unsigned int real_gid; + unsigned int effective_uid; + unsigned int effective_gid; + std::string triple; + std::string ostype; + Endian endian; + unsigned int ptrsize; +}; + +class ThreadInfo { +public: + ThreadInfo(); + ThreadInfo(const std::string& name, const std::string& reason, + const ULongMap& registers, unsigned int signal); + + unsigned long ReadRegister(unsigned long register_id) const; + +private: + std::string name; + std::string reason; + ULongMap registers; + unsigned int signal; +}; + +class JThreadsInfo { +public: + JThreadsInfo(const std::string& response, Endian endian); + + const ThreadInfoMap& GetThreadInfos() const; + +private: + ThreadInfoMap thread_infos; +}; + +class JStopInfo { +public: + JStopInfo(const std::string& response); +}; + +class StopReply { +public: + StopReply(const std::string& response); + + const ULongMap& GetThreadPcs() const; + +private: + unsigned int signal; + unsigned long thread; + std::string name; + std::shared_ptr jstopinfo; + ULongMap thread_pcs; + ULongMap registers; + std::string reason; +}; + +// Common functions for parsing packet data. +std::unordered_map SplitPairList(const std::string& s); +std::vector SplitList(const std::string& s, char delimeter); +std::pair SplitPair(const std::string& s); +std::string HexDecode(const std::string& hex_encoded); +unsigned long SwitchEndian(const std::string& little_endian); +} Index: unittests/tools/lldb-server/tests/MessageObjects.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/MessageObjects.cpp @@ -0,0 +1,207 @@ +#include +#include + +#include "lldb/Core/StructuredData.h" + +#include "MessageObjects.h" +#include "TestClientException.h" + +using namespace std; +using namespace lldb_private; +using namespace llvm; +namespace CommunicationTests { +ProcessInfo::ProcessInfo(const string& response) { + auto elements = SplitPairList(response); + pid = stoi(elements["pid"], nullptr, 16); + parent_pid = stoi(elements["parent-pid"], nullptr, 16); + real_uid = stoi(elements["real-uid"], nullptr, 16); + real_gid = stoi(elements["real-gid"], nullptr, 16); + effective_uid = stoi(elements["effective-uid"], nullptr, 16); + effective_gid = stoi(elements["effective-gid"], nullptr, 16); + triple = HexDecode(elements["triple"]); + if (elements["endian"] == "little") { + endian = LITTLE; + } + else if (elements["endian"] == "big") { + endian = BIG; + } + else { + endian = UNKNOWN; + } + + ptrsize = stoi(elements["ptrsize"], nullptr, 10); +} + +unsigned int ProcessInfo::GetPid() const { + return pid; +} + +Endian ProcessInfo::GetEndian() const { + return endian; +} + +//====== ThreadInfo ============================================================ +ThreadInfo::ThreadInfo() { } +ThreadInfo::ThreadInfo(const string& name, const string& reason, + const ULongMap& registers, unsigned int signal) : + name(name), reason(reason), registers(registers), signal(signal) { +} + +unsigned long ThreadInfo::ReadRegister(unsigned long register_id) const { + return registers.at(register_id); +} + +//====== JThreadsInfo ========================================================== +JThreadsInfo::JThreadsInfo(const string& response, Endian endian) { + StructuredData::ObjectSP json = StructuredData::ParseJSON(response); + StructuredData::Array* array = json->GetAsArray(); + for (size_t i = 0; i < array->GetSize(); i++) { + StructuredData::Dictionary* thread_info; + array->GetItemAtIndexAsDictionary(i, thread_info); + string name; + thread_info->GetValueForKeyAsString("name", name); + string reason; + thread_info->GetValueForKeyAsString("reason", reason); + unsigned long signal; + thread_info->GetValueForKeyAsInteger("signal", signal); + unsigned long tid; + thread_info->GetValueForKeyAsInteger("tid", tid); + + StructuredData::Dictionary* register_dict; + thread_info->GetValueForKeyAsDictionary("registers", register_dict); + ULongMap registers; + + auto keys_obj = register_dict->GetKeys(); + auto keys = keys_obj->GetAsArray(); + for (size_t i = 0; i < keys->GetSize(); i++) { + string key_str; + keys->GetItemAtIndexAsString(i, key_str); + string value_str; + register_dict->GetValueForKeyAsString(key_str, value_str); + unsigned int register_id = stoul(key_str, nullptr, 10); + + if (endian == LITTLE) { + registers[register_id] = SwitchEndian(value_str); + } + else { + registers[register_id] = stoul(value_str, nullptr, 16); + } + } + + thread_infos[tid] = ThreadInfo(name, reason, registers, signal); + } +} + +const ThreadInfoMap& JThreadsInfo::GetThreadInfos() const { + return thread_infos; +} + +//====== JStopInfo ============================================================= +JStopInfo::JStopInfo(const string& response) { + // TODO +} + +//====== StopReply ============================================================= +StopReply::StopReply(const string& response) { + auto elements = SplitPairList(response); + name = elements["name"]; + reason = elements["reason"]; + vector threads = SplitList(elements["threads"], ','); + // jstopinfo = JStopInfo(HexDecode(elements["jstopinfo"])); + vector pcs = SplitList(elements["thread-pcs"], ','); + if (threads.size() != pcs.size()) { + throw TestClientException("Size mismatch between threads and thread-pcs."); + } + + for (unsigned int i = 0; i < threads.size(); i++) { + thread_pcs[stol(threads[i], nullptr, 16)] = stol(pcs[i], nullptr, 16); + } + + unsigned int register_id = 0; + while (true) { + stringstream ss; + ss << hex << setw(2) << setfill('0') << register_id; + string hex_id = ss.str(); + if (elements.find(hex_id) != elements.end()) { + registers[register_id++] = SwitchEndian(elements[hex_id]); + } + else { + break; + } + } + + for (auto i = elements.begin(); i != elements.end(); i++) { + if (i->first[0] == 'T' && i->first.substr(3, 6) == "thread") { + thread = stoul(i->second, nullptr, 16); + signal = stoul(i->first.substr(1, 2), nullptr, 16); + } + } +} + +const ULongMap& StopReply::GetThreadPcs() const { + return thread_pcs; +} + +//====== Globals =============================================================== +unordered_map SplitPairList(const string& s) { + unordered_map pairs; + for (string& s : SplitList(s, ';')) { + pair element = SplitPair(s); + pairs[element.first] = element.second; + } + + return pairs; +} + +vector SplitList(const string& s, char delimeter) { + size_t start = 0; + vector elements; + while (start < s.size()) { + size_t end = s.find_first_of(delimeter, start); + elements.push_back(s.substr(start, end - start)); + if (end == string::npos) { + break; + } + else { + start = end + 1; + } + } + + return elements; +} + +pair SplitPair(const string& s) { + pair element; + size_t colon = s.find_first_of(':'); + if (colon == string::npos) { + return element; + } + + element.first = s.substr(0, colon); + element.second = s.substr(colon + 1); + return element; +} + +string HexDecode(const string& hex_encoded) { + string decoded; + if (hex_encoded.size() % 2 == 1) { + return decoded; + } + + for (size_t i = 0; i < hex_encoded.size(); i += 2) { + unsigned short byte1 = stoi(hex_encoded.substr(i, 2), nullptr, 16); + decoded.push_back((char)byte1); + } + + return decoded; +} + +unsigned long SwitchEndian(const string& little_endian) { + string big_endian; + for (int i = little_endian.size() - 2; i >= 0; i -= 2) { + big_endian += little_endian.substr(i, 2); + } + + return stoul(big_endian, nullptr, 16); +} +} Index: unittests/tools/lldb-server/tests/TestClient.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClient.h @@ -0,0 +1,56 @@ +//===-- TestClient.h --------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include + +#include "lldb/Core/ArchSpec.h" +#include "lldb/Target/ProcessLaunchInfo.h" + +#include "Plugins/Process/gdb-remote/GDBRemoteCommunicationClient.h" + +#include "MessageObjects.h" + +namespace CommunicationTests { +// TODO: Make the test client an abstract base class, with different children +// for different types of connections: llgs v. debugserver +class TestClient : + public lldb_private::process_gdb_remote::GDBRemoteCommunicationClient { +public: + TestClient(const std::string& test_name, const std::string& test_case_name); + virtual ~TestClient(); + void StartDebugger(); + void StopDebugger(); + void SetInferior(const std::vector& inferior_args); + void ListThreadsInStopReply(); + void SetBreakpoint(unsigned long address); + void Continue(unsigned long thread_id = 0); + void StepThread(unsigned long thread_id, int count = 1); + std::shared_ptr GetProcessInfo(); + JThreadsInfo GetJThreadsInfo(); + std::shared_ptr GetLatestStopReply(); + void SendMessage(const std::string& message); + void SendMessage(const std::string& message, std::string& response_string); + unsigned int GetPcRegisterId(); + +private: + void GenerateConnectionAddress(std::string& address); + std::string GenerateLogFileName(const lldb_private::ArchSpec& arch) const; + std::string HexEncode(const std::string& s) const; + std::string FormatFailedResult(const std::string& message, + lldb_private::process_gdb_remote::GDBRemoteCommunication::PacketResult result); + + std::shared_ptr process_info; + std::shared_ptr stop_reply; + lldb_private::ProcessLaunchInfo server_process_info; + std::string test_name; + std::string test_case_name; + unsigned int pc_register; +}; +} Index: unittests/tools/lldb-server/tests/TestClient.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClient.cpp @@ -0,0 +1,239 @@ +//===-- TestClient.cpp ------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include +#include +#include +#include + +#include +#include + +#include "gtest/gtest.h" +#include "TestClient.h" +#include "TestClientException.h" + +#include "lldb/Core/ArchSpec.h" +#include "lldb/Host/common/TCPSocket.h" +#include "lldb/Host/HostInfo.h" +#include "lldb/Host/posix/ConnectionFileDescriptorPosix.h" +#include "lldb/Host/posix/ProcessLauncherPosix.h" +#include "lldb/Interpreter/Args.h" +#include "lldb/Target/ProcessLaunchInfo.h" + +using namespace std; +using namespace lldb; +using namespace lldb_private; +using namespace llvm; + +namespace CommunicationTests { +const char* LOCALHOST = "127.0.0.1"; + +TestClient::TestClient(const string& test_name, const string& test_case_name) +: test_name(test_name), test_case_name(test_case_name), pc_register(UINT_MAX) { + HostInfo::Initialize(); +} + +TestClient::~TestClient() { +} + +void TestClient::StartDebugger() { + const ArchSpec& arch_spec = HostInfo::GetArchitecture(); + Args args; + args.SetCommandString(LLDB_SERVER); + args.AppendArgument("gdbserver"); + args.AppendArgument("--log-file=" + GenerateLogFileName(arch_spec)); + args.AppendArgument("--log-channels=gdb-remote packets"); + args.AppendArgument("--reverse-connect"); + string connectionAddress; + GenerateConnectionAddress(connectionAddress); + + args.AppendArgument(connectionAddress); + server_process_info.SetArchitecture(arch_spec); + server_process_info.SetArguments(args, true); + Error error = Host::LaunchProcess(server_process_info); + ASSERT_FALSE(error.Fail()) + << "Failure to launch lldb server: " << error.AsCString(); + + sleep(5); // TODO: Sleep is bad. Can I wait for it to start? + SendAck(); // Send this as a handshake. +} + +void TestClient::StopDebugger() { + Host::Kill(server_process_info.GetProcessID(), 15); +} + +void TestClient::SetInferior(const vector& inferior_args) { + stringstream command; + command << "A"; + for (size_t i = 0; i < inferior_args.size(); i++) { + if (i > 0) command << ','; + string hex_encoded = HexEncode(inferior_args[i]); + command << hex_encoded.size() << ',' << i << ',' << hex_encoded; + } + + SendMessage(command.str()); + SendMessage("qLaunchSuccess"); + string response; + SendMessage("qProcessInfo", response); + process_info.reset(new ProcessInfo(response)); +} + +void TestClient::ListThreadsInStopReply() { + SendMessage("QListThreadsInStopReply"); +} + +void TestClient::SetBreakpoint(unsigned long address) { + stringstream command; + command << "Z0," << hex << address << ",1"; + SendMessage(command.str()); +} + +void TestClient::Continue(unsigned long thread_id) { + if (thread_id == 0) { + thread_id = process_info->GetPid(); + } + + stringstream message; + message << "vCont;c:" << hex << thread_id; + string response; + SendMessage(message.str(), response); + stop_reply.reset(new StopReply(response)); +} + +void TestClient::StepThread(unsigned long thread_id, int count) { + if (thread_id == 0) { + thread_id = process_info->GetPid(); + } + + stringstream message; + message << "vCont;s:" << hex << thread_id; + string response; + for (int i = 0; i < count; i++) { + SendMessage(message.str(), response); + } + + stop_reply.reset(new StopReply(response)); +} + +shared_ptr TestClient::GetProcessInfo() { + return process_info; +} + +JThreadsInfo TestClient::GetJThreadsInfo() { + string response; + SendMessage("jThreadsInfo", response); + return JThreadsInfo(response, process_info->GetEndian()); +} + +shared_ptr TestClient::GetLatestStopReply() { + return stop_reply; +} + +void TestClient::SendMessage(const string& message) { + string dummy_string; + SendMessage(message, dummy_string); +} + +void TestClient::SendMessage(const string& message, string& response_string) { + StringExtractorGDBRemote response; + PacketResult result = SendPacketAndWaitForResponse(message, response, false); + ASSERT_EQ(result, PacketResult::Success) + << FormatFailedResult(message, result); + + response.GetEscapedBinaryData(response_string); + ASSERT_NE(response_string[0], 'E') + << "Error " << response_string << " while sending message: " << message; +} + +unsigned int TestClient::GetPcRegisterId() { + if (pc_register == UINT_MAX) { + for (unsigned int register_id = 0; ; register_id++) { + stringstream message; + message << "qRegisterInfo" << hex << register_id; + // This will throw if we scan all registers and never get the + // expected result. Is that okay? Or is there a situation where + // no result is valid? + string response; + SendMessage(message.str(), response); + auto elements = SplitPairList(response); + if (elements["alt-name"] == "pc" || elements["generic"] == "pc") { + pc_register = register_id; + break; + } + } + } + + return pc_register; +} + +void TestClient::GenerateConnectionAddress(string& address) { + StartListenThread(LOCALHOST, 0); + auto connection = (ConnectionFileDescriptor*)GetConnection(); + uint16_t listening_port = connection->GetListeningPort(UINT32_MAX); + ASSERT_GT(listening_port, 0) << "GetListeningPort failed."; + + stringstream ss; + ss << LOCALHOST << ":" << listening_port << ends; + address = ss.str(); +} + +string TestClient::GenerateLogFileName(const ArchSpec& arch) const { + stringstream log_file; + log_file << "lldb-test-traces/lldb-" << test_case_name << '-' << test_name + << '-' << arch.GetArchitectureName() << ".log"; + return log_file.str(); +} + +string TestClient::HexEncode(const string& s) const { + stringstream encoded; + for (const char& c : s) { + encoded << hex << (int)c; + } + + return encoded.str(); +} +string TestClient::FormatFailedResult(const string& message, + PacketResult result) { + stringstream ss; + ss << "Failure sending message: " << message << " Result: "; + + switch (result) { + case PacketResult::ErrorSendFailed: + ss << "ErrorSendFailed"; + break; + case PacketResult::ErrorSendAck: + ss << "ErrorSendAck"; + break; + case PacketResult::ErrorReplyFailed: + ss << "ErrorReplyFailed"; + break; + case PacketResult::ErrorReplyTimeout: + ss << "ErrorReplyTimeout"; + break; + case PacketResult::ErrorReplyInvalid: + ss << "ErrorReplyInvalid"; + break; + case PacketResult::ErrorReplyAck: + ss << "ErrorReplyAck"; + break; + case PacketResult::ErrorDisconnected: + ss << "ErrorDisconnected"; + break; + case PacketResult::ErrorNoSequenceLock: + ss << "ErrorNoSequenceLock"; + break; + default: + ss << "Unknown Error"; + } + + return ss.str(); +} +} Index: unittests/tools/lldb-server/tests/TestClientException.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClientException.h @@ -0,0 +1,21 @@ +//===-- TestClient.h --------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include + +namespace CommunicationTests { +class TestClientException : public std::exception { +public: + TestClientException(const std::string& message); + const char* what() const noexcept; +private: + std::string message; +}; +} Index: unittests/tools/lldb-server/tests/TestClientException.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/TestClientException.cpp @@ -0,0 +1,13 @@ +#include "TestClientException.h" + +using namespace std; + +namespace CommunicationTests { +TestClientException::TestClientException(const string& message) : + message(message) { +} + +const char* TestClientException::what() const noexcept { + return message.c_str(); +} +} Index: unittests/tools/lldb-server/tests/ThreadIdsInJstopinfoTest.cpp =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/ThreadIdsInJstopinfoTest.cpp @@ -0,0 +1,52 @@ +//===-- ThreadsInJstopinfoTest.cpp ------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#include +#include + +#include + +#include "gtest/gtest.h" +#include "TestClient.h" + +using namespace std; +using namespace CommunicationTests; + +TEST(ThreadsInJstopinfoTest, TestStopReplyContainsThreadPcsLlgs) { + vector inferior_args; + // This inferior spawns N threads, then forces a break. + inferior_args.push_back(THREAD_INFERIOR); + inferior_args.push_back("4"); + + auto test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + + TestClient client(test_info->name(), test_info->test_case_name()); + client.StartDebugger(); + client.SetInferior(inferior_args); + client.ListThreadsInStopReply(); + client.Continue(); + unsigned int pc_reg = client.GetPcRegisterId(); + + JThreadsInfo jthreads_info = client.GetJThreadsInfo(); + auto stop_reply = client.GetLatestStopReply(); + auto stop_reply_pcs = stop_reply->GetThreadPcs(); + auto thread_infos = jthreads_info.GetThreadInfos(); + + ASSERT_EQ(stop_reply_pcs.size(), thread_infos.size()) + << "Thread count mismatch."; + for (auto stop_reply_pc : stop_reply_pcs) { + unsigned long tid = stop_reply_pc.first; + ASSERT_TRUE(thread_infos.find(tid) != thread_infos.end()) + << "Thread ID: " << tid << " not in JThreadsInfo."; + ASSERT_EQ(stop_reply_pcs[tid], thread_infos[tid].ReadRegister(pc_reg)) + << "Mismatched PC for thread: " << tid; + } + + client.StopDebugger(); +} Index: unittests/tools/lldb-server/tests/gtest_common.h =================================================================== --- /dev/null +++ unittests/tools/lldb-server/tests/gtest_common.h @@ -0,0 +1,26 @@ +//===-- gtest_common.h ------------------------------------------*- C++ -*-===// +// +// The LLVM Compiler Infrastructure +// +// This file is distributed under the University of Illinois Open Source +// License. See LICENSE.TXT for details. +// +//===----------------------------------------------------------------------===// + +#if defined(LLDB_GTEST_COMMON_H) +#error "gtest_common.h should not be included manually." +#else +#define LLDB_GTEST_COMMON_H +#endif + +// This header file is force included by all of LLDB's unittest compilation +// units. Be very leary about putting anything in this file. + +#if defined(_MSC_VER) && (_HAS_EXCEPTIONS == 0) +// Due to a bug in , when _HAS_EXCEPTIONS == 0 the header will try to +// call +// uncaught_exception() without having a declaration for it. The fix for this +// is +// to manually #include , which contains this declaration. +#include +#endif