首页 > 解决方案 > 试图从汇编程序(64 位)的 glibc 调用 C 函数

问题描述

我一直在逐步学习汇编语言:第三版,并在最后一章“走向 C”。我正在尝试获得一种一致的方法来转换 32 位代码,该代码puts在我的 64 位 Ubuntu 系统上调用 C 库 (glibc) 函数。(我想继续阅读文本的最后 50 页,这可能更深入地了解 C [更多令人讨厌的双关语],但来自用 32 位代码编写的汇编库)。代码是:

SECTION .data           ; Section containing initialised data
EatMsg: db "Eat at Joe's!",0

SECTION .text           ; Section containing code
extern puts             ; Simple "put string" routine from clib
global main             ; Required so linker can find entry point
main:
        push ebp        ; Set up stack frame for debugger
        mov ebp,esp
        push ebx        ; Must preserve ebp, ebx, esi, & edi
        push esi
        push edi

;;; Everything before this is boilerplate; use it for all ordinary apps!
        push EatMsg     ; Push address of message on the stack
        call puts       ; Call clib function for displaying strings
        add esp,4       ; Clean stack by adjusting ESP back 4 bytes

;;; Everything after this is boilerplate; use it for all ordinary apps!
        pop edi         ; Restore saved registers
        pop esi
        pop ebx
        mov esp,ebp     ; Destroy stack frame before returning
        pop ebp
        ret             ; Return control to Linux

建议的 nasm 和链接器命令是

nasm -f elf -g -F stabs eatclib.asm
gcc eatclib.o -o eatclib

我找到的最接近解决方案的方法是:Call C functions from 64-bit assembly

我尝试将扩展寄存器转换为rbp,rsp等;在调用之后将堆栈指针调整为 8 位而不是 4 位puts,并使用以下命令调整生成文件:

nasm -f elf64 -g -F dwarf eatclib.asm

gcc eatclib.o -o eatclib -m64 -static

但出现分段错误。

我对 C 调用约定的理解仍然模糊/模糊,以至于当我尝试使用 gdb 调试器时,我并没有真正深入尝试找出错误(这些问题都只是对 32 位有点熟悉约定,而不是 C)。本书旨在为几乎没有 C 语言背景的新手汇编程序员提供介绍性书籍。

在另一个方向尝试,一个简单的 C 程序使用 puts 和一个字符串生成文件(使用 gcc-S选项):

.file   "SayHello.c"
        .text
        .section        .rodata
        .align 8
.LC0:
        .string "This is based on an example from C Primer Plus"
        .text
        .globl  main
        .type   main, @function
main:
.LFB0:
        .cfi_startproc
        pushq   %rbp
        .cfi_def_cfa_offset 16
        .cfi_offset 6, -16
        movq    %rsp, %rbp
        .cfi_def_cfa_register 6
        leaq    .LC0(%rip), %rdi
        call    puts@PLT
        movl    $0, %eax
        popq    %rbp
        .cfi_def_cfa 7, 8
        ret

编译的代码在这里运行(我理解其中的大部分内容,除了.cfi指令、含义.rodata以及为什么 gas 将其卡在 .@PLTputs。)这当然是 gas 语法和我使用的文本主要是 NASM 功能。

我也尝试过使用加载器而不是 gcc,并在Professional Assembly Language (by Richard Blum)的第 89 页上找到了一行

ld -dynamic-linker /lib/ld-linux.so.2 -o eatclib -lc eatclib.o

但最终会出现我之前遇到的非常典型的链接器错误:

ld: i386 architecture of input file `eatclib.o' is incompatible with i386:x86-64 output
ld: warning: cannot find entry symbol _start; defaulting to 0000000000400250
makefile:2: recipe for target 'eatclib' failed

我尝试将-m32选项传递给链接器也无济于事。

无论如何,我正在寻找可行的建议。在我的搜索中,我看到人们建议使用apt-get和安装新的(实际上是旧的)库的示例,但这些似乎有效地破坏了系统范围内的 64 位内容——当我能够运行以前的 32 位代码时,这看起来相当激烈将-melf_i386选项传递给链接器)。

标签: clinuxassemblynasm32bit-64bit

解决方案


要汇编和链接使用 libc 的 64 位 nasm 代码,请键入:

nasm -f elf64 program.asm
gcc -o program program.o

根据您的系统和编程风格,您可能需要传递-no-piegcc以便它接受与位置相关的代码。

不建议在 libc 中链接时直接调用链接器,因为没有稳定的方法可以手动拉入 C 运行时初始化代码。仅仅传递-lc给链接器不足以让 libc 正常工作。

请注意elf64使 nasm 发出 64 位目标文件。gcc 可以在 64 位平台上使用 64 位代码,除非另有说明,因此不需要其他选项。您可能想要添加调试符号,但请记住 stabs 是一种过时的格式。你可能想要这个:

nasm -f elf64 -gdwarf program.asm

机械地转换源代码或多或少是可能的。请记住以下差异:

  • 指针和栈槽长度为 8 字节,所有通用寄存器都扩展为 8 字节;前 8 个寄存器的 64 位变体称为rax, rcx, rdx, rbx, rsp, rbp, rsi, 和rdi.
  • r8存在 8 个新的通用寄存器r15。它们的 32 位、16 位和 8 位版本称为r8d, r8w, r8b` 等。
  • SSE 指令用于浮点而不是 x87 指令
  • 64 位代码通常遵循与 32 位代码不同的调用约定。在 Linux 等类 UNIX 系统上,通常使用amd64 SysV ABI 。rdi在此 ABI 中,标量参数在寄存器、rsirdxrcxr8和中从左到右传递r9。寄存器rbx, rbp, rsp, r12, r13, r14, 和r15必须由被调用者保存,所有其他通用寄存器可以自由覆盖。浮点参数在 SSE 寄存器中传递和返回。如果参数太多,则会在堆栈上传递额外的参数。
  • SysV ABI 要求堆栈指针在函数调用时与 16 字节对齐。由于call指令压入 8 个字节,push rbp而函数序言中的指令又压入 8 个字节,因此默认情况下是这种情况,除非您手动在堆栈上分配空间。请记住以 16 个字节为增量。

这是您问题中的代码转换为 64 位代码。所有更改均已标记:

        SECTION .data
EatMsg: db "Eat at Joe's!",0

        SECTION .text
        extern puts
        global main
main:                           ; function entry (stack alignment: 16 bytes + 8 bytes)
        push rbp                ; setup...
        mov rbp, rsp            ; the stack frame (stack now aligned to 16 bytes + 0 bytes)

                                ; since we have so many registers, I only preserve those
                                ; I want to use and that must be preserved, of which there
                                ; are none in this program.

        lea rdi, [rel EatMsg]   ; load address of EatMsg into rdi
        call puts               ; call puts
                                ; no cleanup needed as we have not pushed anything

        pop rbp                 ; restore rbp
        ret                     ; return

请注意,我遗漏了一堆样板文件。 lea用于加载地址EatMsg而不是更简单的地址,mov rdi, EatMsg因此您的程序与位置无关。如果你不知道这意味着什么,你可以放心地忽略这个花絮,直到以后。

最后,您通常可以忽略 cfi 指令。它们为异常处理添加元数据,这仅在您的代码调用引发异常的 C++ 函数时才重要。它们不会改变代码本身的行为。


推荐阅读