VS2017开发Linux c++程序,调试动态库+远程Attach

作者:刘树伟

一、环境

使用VS2017+安装了CentOS7的VMWare虚拟机,编译x64 Linux C++项目。

二、安装VS2017

安装VS2017的时候,选上“使用C++的Linux开发(Linux development with C++)”组件。

 

vs2017是通过SSH连接到CentOS上调用gdb来调试的,所以要配置SSH连接。执行vs2017菜单栏中的【工具 | 选项】菜单项,在“选项“对话框中,切换到”跨平台“选项,添加SSH连接信息,主机名中输入CentOS IP地址,用户名和密码输入CentOS的用户名和密码(事先关闭CentOS防火墙,或者在CentOS中放行本机)。

在Linux上,要提前装好gdb服务,在Centos上,使用

yum install gdb-gdbserver

来安装(注意,微软官方是使用sudo apt install -y build-essential gdbserver,经测试不好使)

三、创建Linux C++工程

为了代码好管理,我们为我们的项目创建一个文件夹,项目中的所有子工程都放到这个文件夹下,例如文件夹名字为MyProject。不同的项目放到不同的文件夹下管理,看上去更有条理。

3.1 创建可执行工程

vs2017新建Visual C++里的"跨平台(Cross Platform)"中的Linux项目,选“控制台应用程序(Linux)(Console Application(Linux))”.

 

点“确定”按钮后,VS2017自动生成main.cpp。

3.2 创建动态链接库工程

vs2017新建Visual C++里的"跨平台(Cross Platform)"中的Linux项目,选“空项目(Linux)”,创建一个Linux空工程:

在Dynamic工程中,创建一个Dynamic.cpp,里面是动态库需要导出的函数。

注意:在使用vs开发windows c++项目的时候,当我们包含一个头文件时,可以不把这个头文件加到工程中,但开发linux c++项目的时候,由于vs2017是把代码拷贝到CentOS上编译的,如果不把要包含的头文件加到工程中,vs2017是不会把这个头文件拷贝到CentOS上,这将导致编译时找不到文件。所以,任何我们写的代码,只要用到,都需要加到工程中。

注意:如果工程中没有代码,在编译的时候,vs2017不会把工程拷贝到CentOS中,也就是说,CentOS中不会创建Dynamic文件夹。

3.3 创建项目

把TestApp文件夹中的TestApp.sln复制一份到MyProject目录中,并改名为MyProject.sln,使用vs2017打开MyProject.sln。MyProject.sln就是原来的TestApp.sln,且加载了TestApp.vcxproj,由于现在MyProject.sln和TestApp.vcxproj不在同一个目录,所以加载的时候,会弹出如下错误对话框:

点确定即可。

 

正如预期的一样,项目中的TestApp变成不可用:

我们选中它,按键盘的Del键删除这个项目即可。

然后我们把TestApp和Dynamic两个工程加到MyProject项目中:

加完后如下:

设置TestApp为主工程。

3.4 可执行工程调用动态链接库

我们通过动态加载动态库来使用它里面的函数

首先封装加载动态库的函数(放到TestApp工程中的main.cpp即可):

#include <cstdio>

#include <dlfcn.h>

#include <unistd.h>

#include <string>

#include <string.h>

 

typedef void *HMODULE;

typedef int(*FPluginEntryFunc)(void *);

 

#define PATH_MAX 4096

 

void *Linux_LoadLibrary(const char *path, int mode)

{

    void *hModule = NULL;

    dlerror();

    hModule = dlopen(path, mode);

    if (hModule == NULL)

    {

        return NULL;

    }

 

    return hModule;

}

 

void Linux_FreeLibrary(void *hModule)

{

    if (hModule != NULL)

    {

        dlerror();

        if (dlclose(hModule) != 0)

        {

            // failed

        }

    }

    // Ok

}

 

void *Linux_GetProcAddress(void *hModule, const char *funcname)

{

    dlerror();

    void *pFunc = NULL;

    pFunc = dlsym(hModule, funcname);

    if (!pFunc)

    {

        return NULL;

    }

    return pFunc;

}

 

std::string GetCurrentPath()

{

    std::string current_path;

    char current_absolute_path[PATH_MAX] = { 0 };

 

    int cnt = readlink("/proc/self/exe", current_absolute_path, PATH_MAX);

    if (cnt < 0 || cnt >= PATH_MAX)

    {

        if (NULL == realpath("./", current_absolute_path))

        {

            return "";

        }

 

        strcat(current_absolute_path, "/");

        current_path = current_absolute_path;

        return current_path;

    }

 

    int i;

    for (i = cnt; i >= 0; --i)

    {

        if (current_absolute_path[i] == '/')

        {

            current_absolute_path[i + 1] = '\0';

            break;

        }

    }

    current_path = current_absolute_path;

    return current_path;

}

 

然后main函数加载Dynamic.so动态库,并且调用它导出的PluginEntry函数:

int main()

{

    std::string strDynamic = GetCurrentPath() + "Dynamic.so";

    HMODULE hDll = Linux_LoadLibrary(strDynamic.c_str(), RTLD_LAZY);

    if (NULL != hDll)

    {

        FPluginEntryFunc proc = (FPluginEntryFunc)Linux_GetProcAddress(hDll, "PluginEntry");

        if (NULL != proc)

        {

            int nParam = 123;

            int nRet = proc((void *)nParam);

        }

        // 做清理工作,略

    }

 

    printf("hello from TestApp!\n");

    return 0;

}

四、配置工程

默认编译的时候,vs2017会把TestApp和Dynamic两个工程的代码,分别拷贝到CentOS的/root/projects/目录下,如下:

当有另外一个项目中某个工程也叫Dynamic或TestApp时,就可能和咱们的项目冲突,为了解决这个问题,建议为每个项目创建一个不同的文件夹,这样,不同的项目之间就不会冲突了。还记得在创建工程时,为了管理代码,我们在本地创建了一个MyProject文件夹,用来存放TestApp和Dynamic工程的代码吗?在远程CentOS上也使用这个文件夹名是个不错的主意。通过配置工程属性,可以做到这一点,下一节我们介绍如何配置。

 

默认情况下,TestApp编译出来的可执行文件在TestApp/bin/x64/Debug下面,Dynamic编译出来的动态库文件在Dynamic/bin/x64/Debug下面。为了方便调试,我们把这两个工程生成的二进制文件都生成到与这两个文件夹并列的bin目录中,在本地为D:\MyProject\bin\,在CentOS中,为/root/projects/MyProject/bin/。

 

为了通过编译和方便调试,还需要进行一些其它工程配置,下面针对每个工程分别介绍。这里参考了网上的博文:https://www.cnblogs.com/dongc/p/6599461.html

4.1 配置主工程

在工程名上右键,选“属性”,可以弹出工程属性设置对话框:

下面的设置,都是在工程属性设置对话框中进行的。

 

本小节全部是修改TestApp主工程的配置。

  • 首先修改“远程生成根目录”,由“~/projects”改成” /root/projects/$(SolutionName)”。

https://www.cnblogs.com/dongc/p/6599461.html中介绍虽然~/projects也是/root/projects,但在搜索路径的时候,表现不同(本人未验证准确性)。

作用是使TestApp文件夹创建到CentOS的/root/projects/MyProject下面,而不是默认的/root/projects/下面:

  • 修改“输出目录”,使得TestApp生成的二进制文件生成到与TestApp文件夹并列的bin文件夹下面,而不是默认的TestApp/bin下面。

由原来的“$(ProjectDir)bin\$(Platform)\$(Configuration)\”修改为“$(ProjectDir)..\bin\$(Platform)\$(Configuration)\”

(注意:修改“输出目录”后,本地和CentOS上的输出目录同步发生变化)。

4.2 配置动态库工程

  • 修改“配置类型”(也就是工程类型),改成“动态库(.so)”:

修改完成后,先点击“应用”按钮,这样界面上的一些参数的值将会发现变化。

  • 修改“远程生成根目录”,使得Dynamic文件夹在远程创建到/root/projects/MyProject文件夹下(默认在/root/projects下),由”~/projects”改成” /root/projects/$(SolutionName)”

  • 修改“输出目录”,由“$(ProjectDir)bin\$(Platform)\$(Configuration)\”改为“$(ProjectDir)..\bin\$(Platform)\$(Configuration)\”。作用是把编译出的二进制文件由MyProject\Dynamic\bin改为MyProject\bin,这样,Dynamic生成的二进制文件,就和TestApp生成的二进制文件,都生成到MyProject\bin下面了。

  • 修改“目标文件名”,由“lib$(ProjectName)”改为“$(ProjectName)”,作用是把生成的libDynamic.so.1.0改为Dynamic.so.1.0,因为生成的动态库文件,没必要加lib前缀。
  • 修改“目标文件扩展名”,由“.so.1.0”改为“.so”。作用是把生成的Dynamic.so.1.0按Linux文件名惯例,改成Dynamic.so:

最终,CentOS中的目录结构如下:

bin文件中,保存的TestApp和Dynamic两个工程生成的二进制文件。

 

本地目录结构如下:

五、编译调试

5.1 本地启动调试

配置好后编译MyProject项目,提示如下错误:

1>Linking objects

1>D:\MyProject\TestApp\obj\x64\Debug\main.o : error :

1>D:\MyProject\TestApp\main.cpp(7): error : undefined reference to `dlerror'

1>D:\MyProject\TestApp\main.cpp(8): error : undefined reference to `dlopen'

1>D:\MyProject\TestApp\obj\x64\Debug\main.o : error :

1>D:\MyProject\TestApp\main.cpp(21): error : undefined reference to `dlerror'

1>D:\MyProject\TestApp\main.cpp(22): error : undefined reference to `dlclose'

1>D:\MyProject\TestApp\obj\x64\Debug\main.o : error :

1>D:\MyProject\TestApp\main.cpp(32): error : undefined reference to `dlerror'

1>D:\MyProject\TestApp\main.cpp(34): error : undefined reference to `dlsym'

1>collect2 : error : ld returned 1 exit status

 

与windows c++编译错误提示不同,这里的undefined reference to xxx并不表示xxx未定义,而是表示链接不过。我们在CentOS终端中输入:man dlerror,查看一下dlerror函数的说明:

提示我们链接的时候,要加上“-ldl“。这个选项,在vs2017中对应工程属性设置中”附加依赖项“,如下图,加上-ldl选项后,就可以编译通过了:

在CentOS中,生成了TestApp.out和Dynamic.so两个二进制文件,如下:

 

与vs2017调试windows c++项目不同,如果不加断点,直接按F10调试,程序直接就执行完成了,而不是断到main函数入口,所以需要先在main函数入口或者任何你想要调试的代码处加上断点调试,像调试Windows程序一样调试Linux就行。

 

到这里,你已经可以调试main函数了,但会发现,在动态库中加不上断点,在main函数中调用proc的地方按F11,也进入不了动态库中的PluginEntry函数,还需要进行一些额外的配置。

因为我们修改了程序生成的路径,所以在调试选项中,要作相应的修改:

  • 配置“程序”路径,由“$(RemoteTargetPath)”改为“$(RemoteRootDir)/bin/$(Platform)/$(Configuration)/$(TargetName)$(TargetExt)”:

  • 配置“工作目录”,由“$(RemoteOutDir)”修改为” $(RemoteRootDir)/bin/$(Platform)/$(Configuration)”:

  • 配置调试模式,由“gdbserver”改为”gdb”。这一步特别重要

请注意:调试属性页中的选项参数不是保存在工程文件vcxproj中的,而是在vcxproj.user中保存。两个参考值的区别:

  1. 在gdbserver模式下,GDB在本地运行,该数据库连接到远程系统上的gdbserver。
  2. 在gdb模式下,Visual Studio调试器在远程系统上驱动GDB。如果GDB的本地版本与目标计算机上安装的版本不兼容,则这是一个更好的选择。这是Linux控制台窗口支持的唯一模式。

如果无法在gdbserver调试模式下达到断点,请尝试gdb模式。必须先将gdb安装在远程目标上。

 

参考:https://docs.microsoft.com/en-us/cpp/linux/deploy-run-and-debug-your-linux-project?view=vs-2019

MSDN在线目录:Docs \Microsoft C++, C, and Assembler \Linux \Build Linux projects with MSBuild in Visual Studio \Deploy, run, and debug your Linux MSBuild project

 

到此,已经可以在动态库中的函数中下断点,并进行调试了。

如果还不行,可以试试修改附加库目录:在"%(AdditionalLibraryDirectories)"前面添加"$(RemoteRootDir)/bin/$(Platform)/$(Configuration);":

打开Linux控制台窗口查看一些输出信息:

5.2 远程Attach调试

如果我们想先在CentOS上运行TestApp.out后,再通过本地VS2017远程Attach上调试也是可行的。步骤如下:

  • 在远程CentOS上运行TestApp

为了防止我们想要调试的代码在Attach前就被执行,可以在那段代码前加一个死循环:

while(1)

{

sleep(1);

}

  • 在vs2017上,执行菜单项【调试 | 附加到进程】

  • 在“附加到进程”对话框中:

连接类型选SSH,连接和目标选择或输入CentOS的帐号和IP;在“可用进程”列表中,选中TestApp.out后,点“附加”按钮

  • 在“附加到TestApp.out-选择代码类型”中,选择“Native”,因为我们的TestApp是用原生C++,而不是托管C++开发的。

点“确定”按钮Attach到远程TestApp进程中。

  • 设置断点:

在之前添加的死循环中,增加一个断点,这时候,vs2017就会断下来。

跳出死循环:

经测试,使用“设置下一语句”命令试图跳出死循环时,弹出如下错误对话框:

本人现在还不知道如何解决这个问题,如果读者有解决方法,欢迎回复留言,不胜感激。

另一种技巧是使用scanf来中断程序运行:

    char szbuf[256] = { 0 };

    scanf("%s", szbuf);

这样,Attach上去后,只需要在CentOS上输入任意一个字符后按回车,就可以继续运行了。

其它一些有意义的参考:

https://blog.csdn.net/foxriver_gjg1989/article/details/102854440

https://www.cnblogs.com/apocelipes/p/10899484.html#%E6%9C%AC%E5%9C%B0%E7%BC%96%E5%86%99%E5%92%8C%E8%BF%9C%E7%A8%8B%E8%B0%83%E8%AF%95

VisualGDB是一款优秀的在Windows上开发Linux程序的工具,可以研究一下使用VisualGDB来开发Linux程序

常见错误处理:

>> 强烈建议代码文件使用utf-8有签名格式,这样对代码中的中文比较友好。

>> 如果编译时出现包含的头文件提示找不到,把头文件转成utf-8有签名即可。

>> 如果a.cpp中包含了公共库中的x.h,编译a.cpp的时候,如果提示找不到x.h,但你确定在工程属性中(C/C++ | General | Additinal Include Directories和或VC++ Directories)包含了公共库的路径。这时,把x.h加到工程中即可。

>> undefined reference to `pthread_create'

问题的原因:pthread不是linux下的默认的库,也就是在链接的时候,无法找到pthread库中函数的入口地址,于是链接会失败。

解决:如果是用make编译,在gcc编译的时候,附加要加 -lpthread参数即可解决;

在vs2017上,是在工程属性中修改:【Properties | Linker | Input | AddDependencies】中,增加"-lpthread"(无引号),参考:https://blog.csdn.net/wutieliu/article/details/106630427

>> error : Illegal characters in path.

都编译通过了,在生成可执行的*.out文件时,提示error : Illegal characters in path.的话,不一定是配置的输出路径有不合规范的字符,这个有可能是函数只有定义没有实现。

>> error : ld returned 1 exit status

这个是链接错误。表示无法链接到你指定的库。请检查工程属性中【配置属性 | 链接器 | 输入 | 库依赖项】中指定的库是否合法。

>> error : ‘readlink’ was not declared in this scope

这个才是表示readlink函数没有定义,需要包含头文件

>> 如果在单步执行示例代码中的dlopen加载Dynamic.so时导致程序退出

在dlopen后加断点,直接运行,是可以成功运行到dlopen后的断点的,只是单步调试dlopen过不去。如果读者知道原因且有解决方法,请留言回复,不胜感激。

 

  • 4
    点赞
  • 16
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值