7

背景:

我发现自己承担着将 C++ GNU/Linux 应用程序移植到 Windows 的艰巨任务。此应用程序所做的一件事是在特定路径上搜索共享库,然后使用 posix dlopen() 和 dlsym() 调用动态地从中加载类。我们有充分的理由以这种方式加载,我不会在这里讨论。

问题:

要动态发现由 C++ 编译器使用 dlsym() 或 GetProcAddress() 生成的符号,必须使用 extern "C" 链接块来解开它们。例如:

#include <list>
#include <string>

using std::list;
using std::string;

extern "C" {

    list<string> get_list()
    {
        list<string> myList;
        myList.push_back("list object");
        return myList;
    }

}

此代码是完全有效的 C++,可在 Linux 和 Windows 上的众多编译器上编译和运行。但是,它不能与 MSVC 一起编译,因为“返回类型不是有效的 C”。我们想出的解决方法是更改​​函数以返回指向列表的指针而不是列表对象:

#include <list>
#include <string>

using std::list;
using std::string;

extern "C" {

    list<string>* get_list()
    {
        list<string>* myList = new list<string>();
        myList->push_back("ptr to list");
        return myList;
    }

}

我一直在尝试为 GNU/Linux 加载程序找到一个最佳解决方案,该解决方案既可以与新函数和旧旧函数原型一起使用,也可以至少检测何时遇到不推荐使用的函数并发出警告。如果代码只是在尝试使用旧库时出现段错误,这对我们的用户来说是不合适的。我最初的想法是在调用 get_list 期间设置一个 SIGSEGV 信号处理程序(我知道这很恶心 - 我愿意接受更好的想法)。因此,为了确认加载旧库会出现段错误,我认为它会使用旧函数原型(返回列表对象)通过新加载代码(需要指向列表的指针)运行库,令我惊讶的是刚刚工作。我的问题是为什么

下面的加载代码适用于上面列出的两个函数原型。我已经确认它可以在 Fedora 12、RedHat 5.5 和 RedHawk 5.1 上使用 gcc 版本 4.1.2 和 4.4.4。使用带有 -shared 和 -fPIC 的 g++ 编译库,并且可执行文件需要与 dl (-ldl) 链接。

#include <dlfcn.h>
#include <stdio.h>
#include <stdlib.h>
#include <list>
#include <string>

using std::list;
using std::string;

int main(int argc, char **argv)
{
    void *handle;
    list<string>* (*getList)(void);
    char *error;

    handle = dlopen("library path", RTLD_LAZY);
    if (!handle)
    {
        fprintf(stderr, "%s\n", dlerror());
        exit(EXIT_FAILURE);
    }

    dlerror();

    *(void **) (&getList) = dlsym(handle, "get_list");

    if ((error = dlerror()) != NULL)
    {
        printf("%s\n", error);
        exit(EXIT_FAILURE);
    }

    list<string>* libList = (*getList)();

    for(list<string>::iterator iter = libList->begin();
          iter != libList->end(); iter++)
    {
        printf("\t%s\n", iter->c_str());
    }

    dlclose(handle);

    exit(EXIT_SUCCESS);
}
4

1 回答 1

5

正如阿舍普勒所说,这是因为你很幸运。

事实证明,用于 x86 和 x64 的 gcc(和大多数其他编译器)的 ABI 通过将额外的“隐藏”指针 arg 传递给函数来返回“大”结构(太大而无法放入寄存器),该函数使用那个指针作为空间来存储返回值,然后返回指针本身。所以事实证明,形式的函数

struct foo func(...)

大致相当于

struct foo *func(..., struct foo *)

调用者应该为'foo'(可能在堆栈上)分配空间并传递一个指向它的指针。

所以碰巧的是,如果你有一个期望以这种方式调用的函数(期望返回一个结构),而是通过一个返回指针的函数指针调用它,它可能看起来工作——如果垃圾位它获取额外的 arg(调用者留下的随机寄存器内容)碰巧指向可写的地方,被调用的函数会很高兴地在那里写它的返回值,然后返回那个指针,所以被调用的代码会得到看起来像的东西指向它所期望的结构的有效指针。因此,代码可能表面上看起来可以工作,但实际上它可能会破坏一个随机的内存位,这在以后可能很重要。

于 2011-02-24T01:13:25.977 回答