使用Numba生成的汇编代码进行执行

9
在一系列离奇的事件中,我最终陷入了以下困境:我正在使用以下Python代码将由Numba生成的汇编代码写入文件中:
@jit(nopython=True, nogil=True)
def six():
    return 6

with open("six.asm", "w") as f:
    for k, v in six.inspect_asm().items():
        f.write(v)

汇编代码已成功写入文件,但我不知道如何执行它。我尝试了以下方法:
$ as -o six.o six.asm
$ ld six.o -o six.bin
$ chmod +x six.bin
$ ./six.bin

然而,链接步骤失败并出现以下错误:
ld: warning: cannot find entry symbol _start; defaulting to 00000000004000f0
six.o: In function `cpython::__main__::six$241':
<string>:(.text+0x20): undefined reference to `PyArg_UnpackTuple'
<string>:(.text+0x47): undefined reference to `PyEval_SaveThread'
<string>:(.text+0x53): undefined reference to `PyEval_RestoreThread'
<string>:(.text+0x62): undefined reference to `PyLong_FromLongLong'
<string>:(.text+0x74): undefined reference to `PyExc_RuntimeError'
<string>:(.text+0x88): undefined reference to `PyErr_SetString'

我怀疑Numba和/或Python标准库需要动态链接到生成的目标文件中,以使其成功运行,但我不确定如何实现这一点(如果首先可以实现)。

我还尝试了以下方法,其中将中间LLVM代码写入文件而不是汇编:

with open("six.ll", "w") as f:
    for k, v in six.inspect_llvm().items():
        f.write(v)

然后

$ lli six.ll

但是这也失败了,并显示以下错误:
'main' function not found in module.

更新:

事实上存在一种实用工具,可以找到需要传递给ld命令以动态链接Python标准库的相关标志。

$ python3-config --ldflags

返回

-L/Users/rayan/anaconda3/lib/python3.7/config-3.7m-darwin -lpython3.7m -ldl -framework CoreFoundation 

使用正确的标志再次运行以下命令:

$ as -o six.o six.asm
$ ld six.o -o six.bin -L/Users/rayan/anaconda3/lib/python3.7/config-3.7m-darwin -lpython3.7m -ldl -framework CoreFoundation 
$ chmod +x six.bin
$ ./six.bin

我现在正在获得

ld: warning: No version-min specified on command line
ld: entry point (_main) undefined. for inferred architecture x86_64

我尝试在汇编文件中添加了一个_main标签,但似乎没有任何作用。有什么想法来定义入口点吗?

更新2:

以下是汇编代码,如果有用的话,它似乎是带有标签_ZN8__main__7six$241E的目标函数:

    .text
    .file   "<string>"
    .globl  _ZN8__main__7six$241E
    .p2align    4, 0x90
    .type   _ZN8__main__7six$241E,@function
_ZN8__main__7six$241E:
    movq    $6, (%rdi)
    xorl    %eax, %eax
    retq
.Lfunc_end0:
    .size   _ZN8__main__7six$241E, .Lfunc_end0-_ZN8__main__7six$241E

    .globl  _ZN7cpython8__main__7six$241E
    .p2align    4, 0x90
    .type   _ZN7cpython8__main__7six$241E,@function
_ZN7cpython8__main__7six$241E:
    .cfi_startproc
    pushq   %rax
    .cfi_def_cfa_offset 16
    movq    %rsi, %rdi
    movabsq $.const.six, %rsi
    movabsq $PyArg_UnpackTuple, %r8
    xorl    %edx, %edx
    xorl    %ecx, %ecx
    xorl    %eax, %eax
    callq   *%r8
    testl   %eax, %eax
    je  .LBB1_3
    movabsq $_ZN08NumbaEnv8__main__7six$241E, %rax
    cmpq    $0, (%rax)
    je  .LBB1_2
    movabsq $PyEval_SaveThread, %rax
    callq   *%rax
    movabsq $PyEval_RestoreThread, %rcx
    movq    %rax, %rdi
    callq   *%rcx
    movabsq $PyLong_FromLongLong, %rax
    movl    $6, %edi
    popq    %rcx
    .cfi_def_cfa_offset 8
    jmpq    *%rax
.LBB1_2:
    .cfi_def_cfa_offset 16
    movabsq $PyExc_RuntimeError, %rdi
    movabsq $".const.missing Environment", %rsi
    movabsq $PyErr_SetString, %rax
    callq   *%rax
.LBB1_3:
    xorl    %eax, %eax
    popq    %rcx
    .cfi_def_cfa_offset 8
    retq
.Lfunc_end1:
    .size   _ZN7cpython8__main__7six$241E, .Lfunc_end1-_ZN7cpython8__main__7six$241E
    .cfi_endproc

    .globl  cfunc._ZN8__main__7six$241E
    .p2align    4, 0x90
    .type   cfunc._ZN8__main__7six$241E,@function
cfunc._ZN8__main__7six$241E:
    movl    $6, %eax
    retq
.Lfunc_end2:
    .size   cfunc._ZN8__main__7six$241E, .Lfunc_end2-cfunc._ZN8__main__7six$241E

    .type   _ZN08NumbaEnv8__main__7six$241E,@object
    .comm   _ZN08NumbaEnv8__main__7six$241E,8,8
    .type   .const.six,@object
    .section    .rodata,"a",@progbits
.const.six:
    .asciz  "six"
    .size   .const.six, 4

    .type   ".const.missing Environment",@object
    .p2align    4
.const.missing Environment:
    .asciz  "missing Environment"
    .size   ".const.missing Environment", 20


    .section    ".note.GNU-stack","",@progbits

1
我可以问一下你是怎么需要做这个的吗? :D - AKX
1
那听起来很危险,充满着边角情况,我必须承认。 - AKX
2
你能把所有的代码都粘贴一下吗?[SO]: 如何创建一个最小化、可重现的示例(reprex (mcve)) - CristiFati
3
您发布的汇编代码似乎不是完整的程序,而是一个可以作为程序一部分使用的单个函数。当您将此代码作为程序执行时,您期望发生什么? - fuz
1
正如fuz所说,你的汇编程序并不完整。这应该让人感到惊讶,因为Python函数six也不是一个完整的程序。无论你想做什么,你都没有选择正确的方法,并且你选择了错误的工具来完成工作。 - Ross Ridge
显示剩余17条评论
1个回答

10

在浏览了[PyData.Numba]: Numba文档以及进行了一些调试、试错之后,我得出了一个结论:似乎你已经偏离了你的目标(这也在评论中指出了)。

NumbaPython代码(函数)转换为机器代码(出于显而易见的原因:速度)。它会即时完成所有工作(转换、构建、插入运行进程),程序员只需将函数装饰为e.g.@numba.jit[PyData.Numba]: Just-in-Time compilation)。

你所遇到的行为是正确的Dispatcher对象(用于装饰six函数)仅为函数本身生成(汇编)代码(其中没有main,因为代码正在当前进程(Python解释器的main函数)中执行)。因此,链接器报告缺少main符号是正常的。这就像编写仅包含以下内容的C文件:

int six()
{
    return 6;
}

为了让事情正常工作,你需要:
  1. .asm 文件编译成一个 .o(对象)文件(完成)

  2. 将来自 #1..o 文件包含到一个库中,该库可以是:

    • 静态的

    • 动态的


    该库将链接到最终的可执行文件中。此步骤是可选的,因为您可以直接使用 .o 文件。

  3. 构建另一个文件,定义 main(并调用 six - 我认为这是整个目的)成一个 .o 文件。由于我不太熟悉汇编语言,所以我用 C 编写了它。

  4. 将两个实体(来自 #2. (#1.) 和 #3.)链接在一起

作为替代方案,您可以查看[PyData.Numba]:预先编译代码,但请记住,它会生成一个 Python(扩展)模块。

回到当前问题。在 Ubuntu 18.04 64位 上进行了测试。

  • code00.py:

    #!/usr/bin/env python
    
    import math
    import sys
    
    import numba
    
    
    @numba.jit(nopython=True, nogil=True)
    def six():
        return 6
    
    
    def main(*argv):
        six()  # Call the function(s), otherwise `inspect_asm()` would return empty dict
        speed_funcs = [
            (six, numba.int32()),
        ]
        for func, _ in speed_funcs:
            file_name_asm = "numba_{:s}_{:s}_{:03d}_{:02d}{:02d}{:02d}.asm".format(func.__name__, sys.platform, int(round(math.log2(sys.maxsize))) + 1, *sys.version_info[:3])
            asm = func.inspect_asm()
            print("Writing to {:s}:".format(file_name_asm))
            with open(file_name_asm, "wb") as fout:
                for k, v in asm.items():
                    print("    {:}".format(k))
                    fout.write(v.encode())
    
    
    if __name__ == "__main__":
        print("Python {:s} {:03d}bit on {:s}\n".format(" ".join(elem.strip() for elem in sys.version.split("\n")),
                                                       64 if sys.maxsize > 0x100000000 else 32, sys.platform))
        rc = main(*sys.argv[1:])
        print("\nDone.\n")
        sys.exit(rc)
    
  • main00.c:

    #include <dlfcn.h>
    #include <stdio.h>
    
    //#define SYMBOL_SIX "_ZN8__main__7six$241E"
    #define SYMBOL_SIX "cfunc._ZN8__main__7six$241E"
    
    
    typedef int (*SixFuncPtr)();
    
    
    int main()
    {
        void *pMod = dlopen("./libnumba_six_linux.so", RTLD_LAZY);
        if (!pMod) {
            printf("Error (%s) loading module\n", dlerror());
            return -1;
        }
        SixFuncPtr pSixFunc = dlsym(pMod, SYMBOL_SIX);
        if (!pSixFunc) {
            printf("Error (%s) loading function\n", dlerror());
            dlclose(pMod);
            return -2;
        }
        printf("six() returned: %d\n", (*pSixFunc)());
        dlclose(pMod);
        return 0;
    }
    
  • build.sh:

    #!/usr/bin/env bash
    
    CC=gcc
    
    LIB_BASE_NAME=numba_six_linux
    
    FLAG_LD_LIB_NUMBALINUX="-Wl,-L. -Wl,-l${LIB_BASE_NAME}"
    FLAG_LD_LIB_PYTHON="-Wl,-L/usr/lib/python3.7/config-3.7m-x86_64-linux-gnu -Wl,-lpython3.7m"
    
    rm -f *.asm *.o *.a *.so *.exe
    
    echo Generate .asm
    python3 code00.py
    
    echo Assemble
    as -o ${LIB_BASE_NAME}.o ${LIB_BASE_NAME}_064_030705.asm
    
    echo Link library
    LIB_NUMBA="./lib${LIB_BASE_NAME}.so"
    #ar -scr ${LIB_NUMBA} ${LIB_BASE_NAME}.o
    ${CC} -o ${LIB_NUMBA} -shared ${LIB_BASE_NAME}.o ${FLAG_LD_LIB_PYTHON}
    
    echo Dump library contents
    nm -S ${LIB_NUMBA}
    #objdump -t ${LIB_NUMBA}
    
    echo Compile and link executable
    ${CC} -o main00.exe main00.c -ldl
    
    echo Exit script
    

输出:

(py_venv_pc064_03.07.05_test0) [cfati@cfati-ubtu-18-064-00:~/Work/Dev/StackOverflow/q061678226]> ~/sopr.sh
### Set shorter prompt to better fit when pasted in StackOverflow (or other) pages ###

[064bit prompt]>
[064bit prompt]> ls
build.sh  code00.py  main00.c
[064bit prompt]>
[064bit prompt]> ./build.sh
Generate .asm
Python 3.7.5 (default, Nov  7 2019, 10:50:52) [GCC 8.3.0] 064bit on linux

Writing to numba_six_linux_064_030705.asm:
    ()

Done.

Assemble
Link library
Dump library contents
0000000000201020 B __bss_start
00000000000008b0 0000000000000006 T cfunc._ZN8__main__7six$241E
0000000000201020 0000000000000001 b completed.7698
00000000000008e0 0000000000000014 r .const.missing Environment
00000000000008d0 0000000000000004 r .const.six
                 w __cxa_finalize
0000000000000730 t deregister_tm_clones
00000000000007c0 t __do_global_dtors_aux
0000000000200e58 t __do_global_dtors_aux_fini_array_entry
0000000000201018 d __dso_handle
0000000000200e60 d _DYNAMIC
0000000000201020 D _edata
0000000000201030 B _end
00000000000008b8 T _fini
0000000000000800 t frame_dummy
0000000000200e50 t __frame_dummy_init_array_entry
0000000000000990 r __FRAME_END__
0000000000201000 d _GLOBAL_OFFSET_TABLE_
                 w __gmon_start__
00000000000008f4 r __GNU_EH_FRAME_HDR
00000000000006f0 T _init
                 w _ITM_deregisterTMCloneTable
                 w _ITM_registerTMCloneTable
                 U PyArg_UnpackTuple
                 U PyErr_SetString
                 U PyEval_RestoreThread
                 U PyEval_SaveThread
                 U PyExc_RuntimeError
                 U PyLong_FromLongLong
0000000000000770 t register_tm_clones
0000000000201020 d __TMC_END__
0000000000201028 0000000000000008 B _ZN08NumbaEnv8__main__7six$241E
0000000000000820 0000000000000086 T _ZN7cpython8__main__7six$241E
0000000000000810 000000000000000a T _ZN8__main__7six$241E
Compile and link executable
Exit script
[064bit prompt]>
[064bit prompt]> ls
build.sh  code00.py  libnumba_six_linux.so  main00.c  main00.exe  numba_six_linux_064_030705.asm  numba_six_linux.o
[064bit prompt]>
[064bit prompt]> # Run the executable
[064bit prompt]>
[064bit prompt]> ./main00.exe
six() returned: 6
[064bit prompt]>

还有一篇很重要的文章需要发布:numba_six_linux_064_030705.asm

    .text
    .file   "<string>"
    .globl  _ZN8__main__7six$241E
    .p2align    4, 0x90
    .type   _ZN8__main__7six$241E,@function
_ZN8__main__7six$241E:
    movq    $6, (%rdi)
    xorl    %eax, %eax
    retq
.Lfunc_end0:
    .size   _ZN8__main__7six$241E, .Lfunc_end0-_ZN8__main__7six$241E

    .globl  _ZN7cpython8__main__7six$241E
    .p2align    4, 0x90
    .type   _ZN7cpython8__main__7six$241E,@function
_ZN7cpython8__main__7six$241E:
    .cfi_startproc
    pushq   %rax
    .cfi_def_cfa_offset 16
    movq    %rsi, %rdi
    movabsq $.const.six, %rsi
    movabsq $PyArg_UnpackTuple, %r8
    xorl    %edx, %edx
    xorl    %ecx, %ecx
    xorl    %eax, %eax
    callq   *%r8
    testl   %eax, %eax
    je  .LBB1_3
    movabsq $_ZN08NumbaEnv8__main__7six$241E, %rax
    cmpq    $0, (%rax)
    je  .LBB1_2
    movabsq $PyEval_SaveThread, %rax
    callq   *%rax
    movabsq $PyEval_RestoreThread, %rcx
    movq    %rax, %rdi
    callq   *%rcx
    movabsq $PyLong_FromLongLong, %rax
    movl    $6, %edi
    popq    %rcx
    .cfi_def_cfa_offset 8
    jmpq    *%rax
.LBB1_2:
    .cfi_def_cfa_offset 16
    movabsq $PyExc_RuntimeError, %rdi
    movabsq $".const.missing Environment", %rsi
    movabsq $PyErr_SetString, %rax
    callq   *%rax
.LBB1_3:
    xorl    %eax, %eax
    popq    %rcx
    .cfi_def_cfa_offset 8
    retq
.Lfunc_end1:
    .size   _ZN7cpython8__main__7six$241E, .Lfunc_end1-_ZN7cpython8__main__7six$241E
    .cfi_endproc

    .globl  cfunc._ZN8__main__7six$241E
    .p2align    4, 0x90
    .type   cfunc._ZN8__main__7six$241E,@function
cfunc._ZN8__main__7six$241E:
    movl    $6, %eax
    retq
.Lfunc_end2:
    .size   cfunc._ZN8__main__7six$241E, .Lfunc_end2-cfunc._ZN8__main__7six$241E

    .type   _ZN08NumbaEnv8__main__7six$241E,@object
    .comm   _ZN08NumbaEnv8__main__7six$241E,8,8
    .type   .const.six,@object
    .section    .rodata,"a",@progbits
.const.six:
    .asciz  "six"
    .size   .const.six, 4

    .type   ".const.missing Environment",@object
    .p2align    4
".const.missing Environment":
    .asciz  "missing Environment"
    .size   ".const.missing Environment", 20


    .section    ".note.GNU-stack","",@progbits

注意事项:

  • numba_six_linux_064_030705.asm(以及所有从中派生的代码)包含了 six 函数的代码。实际上,有一堆符号(在 OSX 上,您也可以使用本地的 otool -T)比如:

    1. cfunc._ZN8__main__7six$241E - (C) 函数本身

    2. _ZN7cpython8__main__7six$241E - Python 包装器:

      2.1. 执行 C <=> Python 转换(通过 Python API 函数如 PyArg_UnpackTuple

      2.2. 由于 #1. 的原因,它需要(依赖于)libpython3.7m

      2.3. 因此,在这种情况下,nopython=True 没有效果

    此外,这些符号中的 main 部分并不是指一个可执行的入口点(main 函数),而是指一个Python 模块的顶级命名空间(__main__。毕竟,这段代码应该从 Python 中运行。

  • 由于纯 C 函数包含名称中的一个 .),我无法直接从 C 中调用它(因为它是一个无效的标识符名称),所以我不得不手动加载.so 和)函数DlOpen / DlSym),这导致了比简单调用函数更多的代码。
    我没有尝试过,但我认为对生成的 .asm 文件进行以下(手动)更改会简化工作:

    • 在汇编之前将纯 C 函数名称重命名(例如为 __six 或任何其他有效的 C 标识符,也不会与另一个(显式或内部)名称冲突),这将使函数可以直接从 C 中调用。

    • 删除 Python 包装器(#2.)也会摆脱 #2.2.



更新 #0

感谢@PeterCordes分享了我所缺少的精确信息([GNU.GCC]: 控制汇编代码中使用的名称),这里是一个更简单的版本。

main01.c:

#include <stdio.h>

extern int six() asm ("cfunc._ZN8__main__7six$241E");

int main()
{
    printf("six() returned: %d\n", six());
}

输出:

[064bit prompt]> # Resume from previous point + main01.c
[064bit prompt]>
[064bit prompt]> ls
build.sh  code00.py  libnumba_six_linux.so  main00.c  main00.exe  main01.c  numba_six_linux_064_030705.asm  numba_six_linux.o
[064bit prompt]>
[064bit prompt]> ar -scr libnumba_six_linux.a numba_six_linux.o
[064bit prompt]>
[064bit prompt]> gcc -o main01.exe main01.c ./libnumba_six_linux.a -Wl,-L/usr/lib/python3.7/config-3.7m-x86_64-linux-gnu -Wl,-lpython3.7m
[064bit prompt]>
[064bit prompt]> ls
build.sh  code00.py  libnumba_six_linux.a  libnumba_six_linux.so  main00.c  main00.exe  main01.c  main01.exe  numba_six_linux_064_030705.asm  numba_six_linux.o
[064bit prompt]>
[064bit prompt]> ./main01.exe
six() returned: 6
[064bit prompt]>

1
GNU C 扩展可以解决在 asm 名称中使用 . 的问题:将其声明为 void foo(stuff) asm("foo.bar"),以便将 asm 符号名称单独设置为与正常规则不同的名称:https://gcc.gnu.org/onlinedocs/gcc/Asm-Labels.html。应该可以在 GCC / clang / ICC 中使用。或者,是的,也可以更改 asm 源文件,将 foo.bar 替换为 foo_bar,这样就可以工作了。 (不仅在标签上,还要在 .globl foo.bar 指令和其他引用处进行替换。) - Peter Cordes
@PeterCordes:我找了大约一个小时类似的东西!正在尝试。谢谢!!! :) - CristiFati
注意,以 _Z 开头的符号遵循 GNU C++ ABI 的命名规范进行编码。例如 _ZN7cpython8__main__7six$241E 的反编译结果为 cpython::__main__::six$241。标识符中仍然有美元符号($),但至少一些 C++ 编译器允许它作为扩展。另外,请不要创建“更新”或“编辑”部分,只需将您的添加和更正融入您的帖子中,就好像它们一直在那里一样。想查看您的帖子如何更改的任何人都可以查看编辑历史记录。 - Ross Ridge
有点惊讶它在没有初始化Python解释器的情况下能够工作,如果没有它,最终会遇到问题。 - ead
@ead:不完全是。我调用了简单的C函数(如果你看汇编代码,它只有3条指令)。因此没有调用Python API函数,因此也不需要*Py_Initialize**。 - CristiFati

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接