代码之家  ›  专栏  ›  技术社区  ›  Moritz Sigg

头和共享库之间不兼容的函数参数列表不会使我的程序崩溃,为什么?

  •  0
  • Moritz Sigg  · 技术社区  · 11 月前

    在两台不同的机器上构建我的大学项目时,我遇到了LAPACK和英特尔在MKL中的实现问题。我已经找出了主要问题:我使用了来自 liblapacke-devel 这两台机器之间存在差异。在其中一个标头中,每个LAPACK函数都有两个额外的 size_t 参数列表末尾的参数,与LAPACK的Fortran起源有关。意识到这一点,而且由于我无论如何都是在链接MKL,我决定使用MKL头文件,这些头文件在参数列表中没有区别。

    然而,我很好奇,试图调用其中一个LAPACK函数( dsyev )虽然有额外的参数,但仍然链接到MKL,而MKL并没有这些额外的参数。我原本以为会出现segfault或类似的错误,但我的测试程序运行得很好。原因是什么?

    我的理论是,由于参数位于参数列表的末尾,因此在返回调用函数时,它们不会被访问和清理。如果是真的,这是否意味着做这样的事情很好,或者是否有可能导致运行时问题的其他影响?在调用之前,我如何检查可能未对齐的堆栈 dsyev ?

    我试过了 objdump nm 只是意识到关于参数列表的信息从来不是共享库的一部分,只有符号名称。由于我缺乏使用经验 GDB 我也无法从中提取任何有意义的信息。我怎样才能知道我的理论为什么仍然有效?


    以下是相关摘录 lapack.h mkl_lapack.h 提及 dsyev_ ,从两个装置: 集装箱, 背包。h liblapacke-dev ,版本 3.10.0-2ubuntu1 :

    ...
    /* It seems all current Fortran compilers put strlen at end.
    *  Some historical compilers put strlen after the str argument
    *  or make the str argument into a struct. */
    #define LAPACK_FORTRAN_STRLEN_END
    ...
    #define LAPACK_dsyev_base LAPACK_GLOBAL(dsyev,DSYEV)
    void LAPACK_dsyev_base(
        char const* jobz, char const* uplo,
        lapack_int const* n,
        double* A, lapack_int const* lda,
        double* W,
        double* work, lapack_int const* lwork,
        lapack_int* info
    #ifdef LAPACK_FORTRAN_STRLEN_END
        , size_t, size_t
    #endif
    );
    #ifdef LAPACK_FORTRAN_STRLEN_END
        #define LAPACK_dsyev(...) LAPACK_dsyev_base(__VA_ARGS__, 1, 1)
    #else
        #define LAPACK_dsyev(...) LAPACK_dsyev_base(__VA_ARGS__)
    #endif
    

    这导致 dsyev 有两个额外的 size_t 然后由 LAPACK_dsyev 定义。

    主机, 背包。h liblapacke开发 ,版本 3.9.0-1build1 :

    #define LAPACK_dsyev LAPACK_GLOBAL(dsyev,DSYEV)
    void LAPACK_dsyev(
        char const* jobz, char const* uplo,
        lapack_int const* n,
        double* A, lapack_int const* lda,
        double* W,
        double* work, lapack_int const* lwork,
        lapack_int* info );
    

    集装箱, mkl_lapack。h Intel OneMKL ,版本 2024.2 :

    void DSYEV( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                MKL_INT* info ) NOTHROW;
    void dsyev( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                MKL_INT* info ) NOTHROW;
    void dsyev_( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                 const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                 MKL_INT* info ) NOTHROW;
    

    (Capslock C风格、C风格、Fortran风格)

    主机, mkl_lapack。h 英特尔OneMKL ,版本 2021.1.1 :

    void DSYEV( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                MKL_INT* info ) NOTHROW;
    void DSYEV_( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                 const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                 MKL_INT* info ) NOTHROW;
    void dsyev( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                MKL_INT* info ) NOTHROW;
    void dsyev_( const char* jobz, const char* uplo, const MKL_INT* n, double* a,
                 const MKL_INT* lda, double* w, double* work, const MKL_INT* lwork,
                 MKL_INT* info ) NOTHROW;
    

    (Capslock C风格,Capslock Fortran风格,C风格,Fortran风格)

    1 回复  |  直到 11 月前
        1
  •  0
  •   Employed Russian    11 月前

    我原本以为会出现segfault或类似的错误,但我的测试程序运行得很好。

    你的期望是不正确的。在这种情况下,对于大多数调用约定来说,正常工作是预期的行为。

    原因是什么?

    如果你阅读了调用约定,你会明白额外的参数是在额外的寄存器中传递的(或在堆栈上)。

    调用者将这些参数放在那里,但被调用的例程只是 从不看他们 (因为它不期待他们)。

    因此,唯一的影响是调用者执行不必要的操作,并产生微小的开销。

    附笔。

    In C++ 这种不匹配会导致程序因未解析的符号而失败,因为参数的数量和类型被编码到(被篡改的)函数名中。

    P.P.S。

    构建一个这样的例子是微不足道的,例如。

    // foo.h
    int foo(int);
    
    // foo.c
    int foo() { return 42; }
    
    // main.c
    #include <stdio.h>
    #include "foo.h"
    int main() { printf("%d\n", foo(13)); }
    

    编译并链接到 gcc main.c foo.c ,验证它是否打印 42 ,然后拆卸 main 并观察什么 主要的 13 参数,以及如何 foo 忽略该参数。

    P.P.P.S正如PhilMasteG指出的那样 存在 调用约定,其中

    1. 参数在堆栈上传递
    2. 调用者将参数放在那里,被调用者将其清理干净。

    在这样的系统中,呼叫者和被呼叫者之间不匹配的程序确实可能会崩溃。