我使用 Visual Studio 进行远程调试的思路





5.00/5 (7投票s)
我对远程调试的构想及我的实现。
引言
你肯定尝试过使用 Visual Studio 进行远程调试。以下是我的一些想法,我遇到的问题,我提出的解决方案,以及一个免费工具,如果你不想自己实现所有功能,可以使用它。
远程调试配置
远程调试并不难。你将二进制文件及其 pdb 文件部署到目标计算机,然后从 Visual Studio 连接到远程调试器 (msvsmon.exe) 并开始调试。但是,有一些重要的问题,解决这些问题将使你的生活更加轻松,尤其是目标计算机用户是新手的情况下。
即使你可以控制远程计算机(例如,通过 TeamViewer 或我自己的 HMN 会话),此处介绍的构想也将帮助你提高远程调试的效率。
问题 #1:是否安装了 Visual Studio 远程工具?
这很简单。远程调试器的安装路径可以在以下位置找到:
- HKLM\SOFTWARE\Microsoft\VisualStudio\14.0 - InstallDir,或
- HKLM\SOFTWARE\Microsoft\VisualStudio\14.0\EnterpriseTools\QualityTools - InstallDir
将以下内容附加到此目录:
- \Remote Debugger\x64\msvsmon.exe 或
- \Remote Debugger\x86\msvsmon.exe
如果在此位置找不到可执行文件,则需要安装远程工具。截至撰写本文时,你可以从以下位置获取它们:
- https://download.microsoft.com/download/E/7/A/E7AEA696-A4EB-48DD-BA4A-9BE41A402400/rtools_setup_x64.exe
- https://download.microsoft.com/download/E/7/A/E7AEA696-A4EB-48DD-BA4A-9BE41A402400/rtools_setup_x86.exe
它们具有用于静默/无人值守安装的不错的命令行参数。
问题 #2:MSVSMON 需要传入连接
目标 PC 通常位于某些愚蠢的非 UPnP NAT 或某些限制性防火墙或杀毒软件之后,因此你将很难指导你的客户端配置所有这些内容,或者你将被迫通过远程会话操作未知的路由器。
如果可以反转连接,以便 msvsmon 连接到你的 PC,那就容易多了。但是 msvsmon 不支持这样做。
解决方案是使用我的连接反转方法(一个简单的中间人)。假设你的客户端监听端点为 A1 (msvsmon),你希望从你的 B1 (Visual Studio) 连接,但 NAT 阻止了它。然后你启动以下设置:
- 一个可通过你的 NAT 访问的 B2 监听点。
- 一个在你的局域网内使用的 B3 监听点,并与 B2 内部通信。
- 远程计算机中的一个 A2 点。
然后,会发生什么?
- A2 连接到 B2
- A2 连接到 A1
- B1 连接到 B3。B3 收到的所有流量都会转发到 B2,B2 会将其转发到 A2,A2 会将其转发到 A1。A1 和 B1(你无法控制的端点,msvsmon 和 Visual Studio)不知道此设置,并认为它们直接通信。
这是我对上述设置的实现
class CREV { public: XSOCKET s2; XSOCKET c1; bool Compression = false; #pragma pack(push,1) enum P2PTYPE { CONNECT, CONNECTRESULT, DATA, CDATA, TERMINATE, }; struct P2P { int Type = 0; unsigned long long ID = 0; int sz = 0; int res = 0; }; #pragma pack(pop) CREV() { __nop(); } int Port1 = 0; int Port2 = 0; unordered_map<unsigned long long, tuple<XSOCKET>> sessions; size_t EraseSessionFromThread(unsigned long long id) { sessions.erase(id); return 0; }; void CompressB(vector<char>& d) { ZIPUTILS::ZIP z(d.data(), d.size()); vector<char> dd; unsigned long long beforecompression = d.size(); z.MemCompress(d.data(), d.size(), dd); dd.resize(dd.size() + 8); memcpy(dd.data() + dd.size() - 8, &beforecompression, 8); d = dd; } void UncompressB(vector<char>& d) { if (d.size() <= 8) return; unsigned long long bc = 0; memcpy(&bc, d.data() + d.size() - 8, 8); d.resize(d.size() - 8); ZIPUTILS::ZIP z(d.data(), d.size()); vector<char> dd; dd.resize(bc + 100); z.MemUncompress(d.data(), d.size(), dd); d = dd; } unsigned long long TotalBytesTransmitted = 0; void EstablishedConnection(unsigned long long sid, int Mode) { auto& s = sessions[sid]; auto& x = get<0>(s); vector<char> b(10000); for (;;) { b.resize(10000); int rv = x.receive(b.data(), 10000); if (rv == 0 || rv == -1) break; b.resize(rv); if (Compression) CompressB(b); // Notify of data XSOCKET& aa = (Mode == 0) ? s2 : c1; P2P p; p.Type = DATA; if (Compression) p.Type = CDATA; p.ID = sid; p.sz = b.size(); if (aa.transmit((char*)&p, sizeof(p), true) != sizeof(p)) break; if (aa.transmit((char*)b.data(), b.size(), true) != b.size()) break; TotalBytesTransmitted += b.size(); } // Notify of termination XSOCKET& aa = (Mode == 0) ? s2 : c1; P2P p; p.Type = TERMINATE; p.ID = sid; p.sz = 0; aa.transmit((char*)&p, sizeof(p), true); // Kill session x.Close(); UWL::InterlockedCall2<size_t, CREV*,unsigned long long>(std::forward<std::function<size_t(CREV*,unsigned long long)>>(&CREV::EraseSessionFromThread), this,std::forward<unsigned long long>(sid)); } void ServerListen1(int p) { UWL::XSOCKET s; if (!s.BindAndListen(p)) return; for (;;) { auto y2 = s.Accept(); if (y2 == INVALID_SOCKET) break; XSOCKET y = y2; // We order s2 to send a "connect request" if (!s2.Valid()) { y.Close(); continue; } // UWL::debugprint("s1 accepted client, we will order a connect request\r\n"); P2P p; p.Type = CONNECT; p.ID = GetTickCount64(); p.sz = 0; // Create the session sessions[p.ID] = make_tuple<>(y); if (s2.transmit((char*)&p, sizeof(p), true) != sizeof(p)) { y.Close(); continue; } } } void ServerListen2(int port) { // This accepts connections from remote peer (control) UWL::XSOCKET s; if (!s.BindAndListen(port)) return; s2 = s.Accept(); if (s2.operator SOCKET() == INVALID_SOCKET) return; s.Close(); P2P p; vector<char> b; for (;;) { int rv = s2.receive((char*)&p, sizeof(P2P)); if (rv == 0 || rv == -1) break; if (p.sz) { b.resize(p.sz); rv = s2.receive((char*)b.data(), p.sz, true); if (rv != p.sz) break; } if (p.Type == TERMINATE) { // Server orders us to terminate get<0>(sessions[p.ID]).Close(); } if (p.Type == DATA) { // Send the received data from the server to our client auto& sock = get<0>(sessions[p.ID]); sock.transmit((char*)b.data(), b.size(), true); } if (p.Type == CDATA) { // Send the received data from the server to our client UncompressB(b); auto& sock = get<0>(sessions[p.ID]); sock.transmit((char*)b.data(), b.size(), true); } if (p.Type == CONNECTRESULT) { // Client notifies us that connection succeeded or failed if (p.res == 0) { // OK std::thread t(&CREV::EstablishedConnection, this,p.ID, 0); t.detach(); } else { // Kill session UWL::InterlockedCall2<size_t, CREV*, unsigned long long>(std::forward<std::function<size_t(CREV*, unsigned long long)>>(&CREV::EraseSessionFromThread), this, std::forward<unsigned long long>(p.ID)); } } } s2.Close(); } void Server(int p1,int p2) { std::thread t1(&CREV::ServerListen1,this, p1); t1.detach(); std::thread t2(&CREV::ServerListen2,this, p2); t2.detach(); } void Client(string server, int sport, string lserver, int lport) { // Connect to server XSOCKET Link; if (!Link.ConnectTo(server.c_str(), sport)) return; c1 = Link; Link.CloseIf(); P2P p; vector<char> b; for (;;) { int rv = c1.receive((char*)&p, sizeof(P2P)); if (rv == 0 || rv == -1) break; if (p.sz) { b.resize(p.sz); rv = c1.receive((char*)b.data(), p.sz, true); if (rv != p.sz) break; } if (p.Type == CONNECT) { // Server orders us to connect XSOCKET a; if (!a.ConnectTo(lserver.c_str(), lport)) { // Failed .... P2P p2; p2.Type = CONNECTRESULT; p2.ID = p.ID; p2.sz = 0; p2.res = -1; if (c1.transmit((char*)&p2, sizeof(p2), true) != sizeof(p2)) break; continue; } // Success sessions[p.ID] = make_tuple<>(a); P2P p2; p2.Type = CONNECTRESULT; p2.ID = p.ID; p2.sz = 0; p2.res = 0; if (c1.transmit((char*)&p2, sizeof(p2), true) != sizeof(p2)) break; std::thread t(&CREV::EstablishedConnection, this,p.ID, 1); t.detach(); } if (p.Type == TERMINATE) { // Server orders us to terminate get<0>(sessions[p.ID]).Close(); } if (p.Type == DATA) { // Send the received data from the server to our client auto& sock = get<0>(sessions[p.ID]); sock.transmit((char*)b.data(), b.size(), true); } if (p.Type == CDATA) { // Send the received data from the server to our client UncompressB(b); auto& sock = get<0>(sessions[p.ID]); sock.transmit((char*)b.data(), b.size(), true); } } c1.Close(); } };
此实现使用了各种组件:Zip Utils,我自己的原子调用和一个简单的 SOCKET 包装器。
问题 #3:MSVSMON 流量未压缩
这可以通过我的上述设置轻松解决。A2 和 B2 之间的流量(网络瓶颈)可以被压缩,然后在将其传递到 A1 和 B3 之前解压缩。这样,未压缩的流量只通过本地网络。
在上面的代码中,你只需要启用压缩标志。压缩对于 A1 或 B1 是透明的。
问题 #4:部署非常慢。
一个简单的可执行文件可能约为 1MB,以及一个 6MB 的 pdb 文件。较大的二进制文件可能有大量的 MB 需要传输,即使代码发生最细微的更改(调试中非常常见),也需要部署整个 PDB。
你已经通过压缩节省了很多,但是,仍然必须做一些事情来避免仅在存在少量更改时重新部署整个文件。
这次我的DIFF 库可以帮助你。它基于远程差分压缩 API,仅部署二进制文件的更改,从而节省大量的带宽和时间。
以下是针对你想要部署的每个文件的算法:
- 服务器请求客户端提供现有文件的签名。
- 如果找不到该文件,服务器会将整个文件上传到客户端。
- 如果客户端找到该文件,它将计算签名并将其传输到服务器。
-
DIFFLIB::DIFF diff; HANDLE hX = CreateFile(LastFile.c_str() , GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); DIFFLIB::FileRdcFileReader fi(hX); DIFFLIB::MemoryDiffWriter wr; diff.GenerateSignature(&fi, wr); // wr.p() contains the signature in a vector of char. CloseHandle(hX);
- 服务器生成本地(较新文件)的签名,然后计算差异并将其发送到客户端。
-
HANDLE hX = CreateFile(a.local.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); DIFFLIB::FileRdcFileReader MyFile(hX); DIFFLIB::MemoryDiffWriter MySignature; // say it was stored in some buffer d DIFFLIB::MemoryRdcFileReader RemoteSig(d.data(), d.size()); DIFFLIB::MemoryDiffWriter diffw; diff.GenerateDiff(&RemoteSig, MySignature.GetReader(), &MyFile, diffw);
- 然后,客户端使用旧文件和差异重建文件。
-
HANDLE hY = CreateFile(File.c_str(), GENERIC_READ, FILE_SHARE_READ, 0, OPEN_EXISTING, 0, 0); DIFFLIB::FileRdcFileReader r1(hY); DIFFLIB::MemoryRdcFileReader diffx(d.data(), d.size()); DIFFLIB::MemoryDiffWriter reco; diff.Reconstruct(&r1, &diffx, 0, reco); CloseHandle(hY); // And now save reco to the file hY = CreateFile(LastFile.c_str(), GENERIC_WRITE, 0, 0, CREATE_ALWAYS, 0, 0); DWORD A = 0; WriteFile(hY, reco.p().data(), (int)reco.sz(), &A, 0); CloseHandle(hY);
然后你就可以完成了。你已成功修补了文件。
我的工具
我已经为个人程序员实现了一个免费工具,远程调试轻松。欢迎使用!
祝你好运。
历史
2016 年 3 月 20 日:首次发布。