7

バックグラウンド:

私は、C++ GNU/Linux アプリケーションを Windows に移植するという、うらやましい仕事をしていることに気づきました。このアプリケーションが行うことの 1 つは、特定のパスで共有ライブラリを検索し、posix dlopen() および dlsym() 呼び出しを使用してそれらからクラスを動的にロードすることです。このようにロードするのには十分な理由がありますが、ここでは触れません。

問題:

dlsym() または GetProcAddress() を使用して C++ コンパイラによって生成されたシンボルを動的に検出するには、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 の両方の多数のコンパイラでコンパイルおよび実行されます。ただし、「戻り値の型が有効な C ではない」ため、MSVC ではコンパイルされません。私たちが思いついた回避策は、リスト オブジェクトの代わりにリストへのポインターを返すように関数を変更することです。

#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 シグナル ハンドラーを設定することでした (これが厄介なのはわかっています。より良いアイデアを受け入れます)。したがって、古いライブラリをロードするとセグメンテーション違反が発生することを確認するために、古い関数プロトタイプ (リスト オブジェクトを返す) を使用して新しいロード コード (リストへのポインタを期待する) を使用してライブラリを実行し、驚いたことにそれを実行しました。ちょうど働いた。私が持っている質問は、なぜですか?

以下の読み込みコードは、上記の両方の関数プロトタイプで動作します。gcc バージョン 4.1.2 および 4.4.4 を使用して、Fedora 12、RedHat 5.5、および RedHawk 5.1 で動作することを確認しました。-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' (おそらくスタック上) にスペースを割り当て、それへのポインターを渡すことが期待されます。

したがって、この方法で呼び出されることを期待している (構造体を返すことを期待している) 関数があり、代わりにポインターを返す関数ポインターを介して呼び出す場合、それが機能しているように見える場合があります-ゴミがそれをビット化した場合追加の引数 (呼び出し元がそこに残したランダムなレジスタの内容) がたまたま書き込み可能な場所を指している場合、呼び出された関数はそこに戻り値を喜んで書き込み、そのポインターを返すため、呼び出されたコードは次のようなものを返します。期待している構造体への有効なポインタ。そのため、コードは表面的には機能しているように見えますが、実際には、後で重要になるメモリのランダムなビットを破壊している可能性があります。

于 2011-02-24T01:13:25.977 に答える