在64位系统上使用GNU工具链组装32位二进制文件

19

我编写了汇编代码并成功编译:

as power.s -o power.o

然而,当我尝试链接目标文件时,它失败了:

ld power.o -o power

为了在64位操作系统(Ubuntu 14.04)上运行,我在power.s文件开头添加了.code32,但我仍然收到如下错误信息:

分段错误(core dumped)

power.s:
.code32
.section .data
.section .text
.global _start
_start:
pushl $3
pushl $2 
call power 
addl $8, %esp
pushl %eax 

pushl $2
pushl $5
call power
addl $8, %esp

popl %ebx
addl %eax, %ebx

movl $1, %eax
int $0x80



.type power, @function
power:
pushl %ebp  
movl %esp, %ebp 
subl $4, %esp 
movl 8(%ebp), %ebx 
movl 12(%ebp), %ecx 
movl %ebx, -4(%ebp) 

power_loop_start:
cmpl $1, %ecx 
je end_power
movl -4(%ebp), %eax
imull %ebx, %eax
movl %eax, -4(%ebp)

decl %ecx
jmp power_loop_start

end_power:
movl -4(%ebp), %eax 
movl %ebp, %esp
popl %ebp
ret

1
你已经做了什么来解决这个问题了吗?你尝试使用调试器逐步执行代码以查看它在哪里崩溃了吗?你知道什么是分段错误吗?(与在64位系统上运行32位代码无关)。 - David Hoelzer
@PeterCordes:我同意segfault的大问题将是32位代码作为64位程序运行,但我不认为David的分析是正确的(我不相信ret存在堆栈问题会导致segfault)。2^3 + 5^2的结果应该是33(它似乎是这样的)。 - Michael Petch
再看一遍,你是对的@MichaelPetch。今天早上我一开始看错了%ebp的引用。 - David Hoelzer
@DavidHoelzer:没问题,David。我也经历过那些事情! - Michael Petch
感谢大家的帮助。正如PeterCordes和MichaelPetch所说,这个问题是由“32位代码作为64位程序运行”引起的。我按照PeterCorde的方法进行操作:gcc -m32 -static -nostartfiles power.S -o power 然后我成功地运行了power。-m32选项允许在x86-64平台上生成32位代码。然而,我不知道为什么要使用-static选项来静态链接程序。我也没有找到-nostartfiles选项的含义。@PeterCordes - buweilv
显示剩余2条评论
2个回答

22
TL:DR: 使用 gcc -m32 -static -nostdlib foo.S(或等效的 as 和 ld 选项)。如果您没有定义自己的 _start,只需使用gcc -m32 -no-pie foo.S 如果您链接 libc,则可能需要安装 gcc-multilib,或者按照您发行版的方式打包 /usr/lib32/libc.so/usr/lib32/libstdc++.so 等。但如果您定义了自己的 _start 并且不链接库,则不需要库包,只需要支持 32 位进程和系统调用的内核。这包括大多数发行版,但不包括 Windows Subsystem for Linux v1。
不要使用 .code32.code32并不会改变输出文件格式,而这决定了程序运行的模式。你需要避免在64位模式下尝试运行32位代码。 .code32用于汇编一些既有16位又有32位代码的内核等等。如果你不是这样做的,请避免使用它,否则就会在构建错误模式的.S时获得构建时错误,例如如果有任何pushpop指令。使用.code32只会让你创建混乱且难以调试的运行时问题,而不是构建时错误。
建议:对于手写汇编,请使用.S扩展名。(例如,gcc -c foo.S将在as之前通过C预处理器运行它,因此您可以#include <sys/syscall.h>获取系统调用号码)。此外,它可以将其与.s编译器输出(从gcc foo.c -O3 -S)区分开来。
要构建32位二进制文件,请使用以下命令之一。
gcc -g foo.S -o foo -m32 -nostdlib -static  # static binary with absolutely no libraries or startup code
                       # -nostdlib still dynamically links when Linux where PIE is the default, or on OS X

gcc -g foo.S -o foo -m32 -no-pie            # dynamic binary including the startup boilerplate code.
     # Use with code that defines a main(), not a _start

nostdlib, -nostartfiles-static的文档


_start中使用libc函数(参见本答案末尾的示例)

一些函数,例如malloc(3)或包括printf(3)在内的stdio函数,依赖于某些全局数据被初始化(例如FILE *stdout及其实际指向的对象)。

gcc -nostartfiles省略了CRT _start样板代码,但仍然链接了libc(默认情况下是动态链接)。在Linux上,共享库可以具有初始化器部分,在动态链接器加载它们时运行,然后跳转到您的_start入口点之前。因此,gcc -nostartfiles hello.S仍然允许您调用printf对于动态可执行文件,内核会在其中运行/lib/ld-linux.so.2而不是直接运行它(使用readelf -a查看二进制文件中的“ELF解释器”字符串)。当您的_start最终运行时,并非所有寄存器都将被清零,因为动态链接器在您的进程中运行了代码。

然而,如果您调用printf或其他函数而不调用glibc的内部初始化函数,则gcc -nostartfiles -static hello.S会链接但在运行时崩溃(请参见Michael Petch的评论)。
当然,你可以将任意组合的.c.S.o文件放在同一条命令行中,将它们链接成一个可执行文件。如果你有任何C代码,请不要忘记使用-Og -Wall -Wextra:你不想在调试汇编语言时发现问题其实是C代码中的简单错误,而编译器本来可以提醒你的。
使用-v可以让gcc显示出用于汇编和链接的命令。如果要手动完成此操作:
as foo.S -o foo.o -g --32 &&      # skips the preprocessor
ld -o foo foo.o  -m elf_i386

file foo
foo: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, not stripped

"

gcc -nostdlib -m32比as和ld的两个不同选项(--32-m elf_i386)更易于记忆和输入。此外,它适用于所有平台,包括可执行文件格式不是ELF的平台。(但是,在OS X上,系统调用号码不同,因此Linux示例无法工作;在Windows上,它甚至不使用int 0x80 ABI。)

"

NASM/YASM

gcc无法处理NASM语法。(-masm=intel更像MASM而非NASM语法,需要使用offset symbol来获取地址作为立即数。)当然,指令也是不同的(例如.globlglobal)。

您可以使用nasmyasm进行构建,然后像上面一样使用gcc链接.o,或直接使用ld

我使用一个包装脚本来避免重复键入相同文件名的三个不同扩展名。(nasm和yasm默认生成file.asm -> file.o, 而不像GNU as的默认输出a.out)。搭配-m32 使用可以汇编和链接 32位 ELF 可执行文件。并非所有操作系统都使用 ELF,所以这个脚本比使用gcc -nostdlib -m32进行链接要不太可移植。

#!/bin/bash
# usage: asm-link [-q] [-m32] foo.asm  [assembler options ...]
# Just use a Makefile for anything non-trivial.  This script is intentionally minimal and doesn't handle multiple source files
# Copyright 2020 Peter Cordes.  Public domain.  If it breaks, you get to keep both pieces

verbose=1                       # defaults
fmt=-felf64
#ldopt=-melf_i386
ldlib=()

linker=ld
#dld=/lib64/ld-linux-x86-64.so.2
while getopts 'Gdsphl:m:nvqzN' opt; do
    case "$opt" in
        m)  if [ "m$OPTARG" = "m32" ]; then
                fmt=-felf32
                ldopt=-melf_i386
                #dld=/lib/ld-linux.so.2  # FIXME: handle linker=gcc non-static executable
            fi
            if [ "m$OPTARG" = "mx32" ]; then
                fmt=-felfx32
                ldopt=-melf32_x86_64
            fi
            ;;
        #   -static
        l)  linker="gcc -no-pie -fno-plt -nostartfiles"; ldlib+=("-l$OPTARG");;
        p)  linker="gcc -pie -fno-plt -nostartfiles"; ldlib+=("-pie");;
        h)  ldlib+=("-Ttext=0x200800000");;   # symbol addresses outside the low 32.  data and bss go in range of text
                          # strace -e raw=write  will show the numeric address
        G)  nodebug=1;;      # .label: doesn't break up objdump output
        d)  disas=1;;
        s)  runsize=1;;
        n)  use_nasm=1 ;;
        q)  verbose=0 ;;
        v)  verbose=1 ;;
        z)  ldlib+=("-zexecstack") ;;
        N)  ldlib+=("-N") ;;   # --omagic = read+write text section
    esac
done
shift "$((OPTIND-1))"   # Shift off the options and optional --

src=$1
base=${src%.*}
shift

#if [[ ${#ldlib[@]} -gt 0 ]]; then
    #    ldlib+=("--dynamic-linker" "$dld")
    #ldlib=("-static" "${ldlib[@]}")
#fi

set -e
if (($use_nasm)); then
  #  (($nodebug)) || dbg="-g -Fdwarf"     # breaks objdump disassembly, and .labels are included anyway
    ( (($verbose)) && set -x    # print commands as they're run, like make
    nasm "$fmt" -Worphan-labels $dbg  "$src" "$@" &&
        $linker $ldopt -o "$base" "$base.o"  "${ldlib[@]}")
else
    (($nodebug)) || dbg="-gdwarf2"
    ( (($verbose)) && set -x    # print commands as they're run, like make
    yasm "$fmt" -Worphan-labels $dbg "$src" "$@" &&
        $linker $ldopt -o "$base" "$base.o"  "${ldlib[@]}" )
fi

# yasm -gdwarf2 includes even .local labels so they show up in objdump output
# nasm defaults to that behaviour of including even .local labels

# nasm defaults to STABS debugging format, but -g is not the default

if (($disas));then
    objdump -drwC -Mintel "$base"
fi

if (($runsize));then
    size $base
fi

我更喜欢YASM,有几个原因,其中包括它默认制作长的nop而不是用许多单字节的nop填充。这会导致混乱的反汇编输出,并且如果nops运行,速度会变慢。(在NASM中,你必须使用smartalign宏包。)

然而,YASM已经有一段时间没有得到维护了,只有NASM支持AVX512; 现在我更经常使用NASM。


示例:使用libc函数从_start开始的程序

# hello32.S

#include <asm/unistd_32.h>   // syscall numbers.  only #defines, no C declarations left after CPP to cause asm syntax errors

.text
#.global main   # uncomment these to let this code work as _start, or as main called by glibc _start
#main:
#.weak _start

.global _start
_start:
        mov     $__NR_gettimeofday, %eax  # make a syscall that we can see in strace output so we know when we get here
        int     $0x80

        push    %esp
        push    $print_fmt
        call   printf

        #xor    %ebx,%ebx                 # _exit(0)
        #mov    $__NR_exit_group, %eax    # same as glibc's _exit(2) wrapper
        #int    $0x80                     # won't flush the stdio buffer

        movl    $0, (%esp)   # reuse the stack slots we set up for printf, instead of popping
        call    exit         # exit(3) does an fflush and other cleanup

        #add    $8, %esp     # pop the space reserved by the two pushes
        #ret                 # only works in main, not _start

.section .rodata
print_fmt: .asciz "Hello, World!\n%%esp at startup = %#lx\n"

$ gcc -m32 -nostdlib hello32.S
/tmp/ccHNGx24.o: In function `_start':
(.text+0x7): undefined reference to `printf'
...
$ gcc -m32 hello32.S
/tmp/ccQ4SOR8.o: In function `_start':
(.text+0x0): multiple definition of `_start'
...

运行时失败,因为没有调用glibc初始化函数。根据Michael Petch的评论,这些函数包括__libc_init_first__dl_tls_setup__libc_csu_init,按照这个顺序执行。其他的libc实现也存在,包括MUSL,它专门为静态链接而设计,并且可以在没有初始化调用的情况下工作。
$ gcc -m32 -nostartfiles -static hello32.S     # fails at run-time
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (GNU/Linux), statically linked, BuildID[sha1]=ef4b74b1c29618d89ad60dbc6f9517d7cdec3236, not stripped
$ strace -s128 ./a.out
execve("./a.out", ["./a.out"], [/* 70 vars */]) = 0
[ Process PID=29681 runs in 32 bit mode. ]
gettimeofday(NULL, NULL)                = 0
--- SIGSEGV {si_signo=SIGSEGV, si_code=SI_KERNEL, si_addr=0} ---
+++ killed by SIGSEGV (core dumped) +++
Segmentation fault (core dumped)

你也可以使用 gdb ./a.out,然后运行 b _startlayout regrun,看看会发生什么。
$ gcc -m32 -nostartfiles hello32.S             # Correct command line
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked, interpreter /lib/ld-linux.so.2, BuildID[sha1]=7b0a731f9b24a77bee41c13ec562ba2a459d91c7, not stripped

$ ./a.out
Hello, World!
%esp at startup = 0xffdf7460

$ ltrace -s128 ./a.out > /dev/null
printf("Hello, World!\n%%esp at startup = %#lx\n", 0xff937510)      = 43    # note the different address: Address-space layout randomization at work
exit(0 <no return ...>
+++ exited (status 0) +++

$ strace -s128 ./a.out > /dev/null        # redirect stdout so we don't see a mix of normal output and trace output
execve("./a.out", ["./a.out"], [/* 70 vars */]) = 0
[ Process PID=29729 runs in 32 bit mode. ]
brk(0)                                  = 0x834e000
access("/etc/ld.so.nohwcap", F_OK)      = -1 ENOENT (No such file or directory)
....   more syscalls from dynamic linker code
open("/lib/i386-linux-gnu/libc.so.6", O_RDONLY|O_CLOEXEC) = 3
mmap2(NULL, 1814236, PROT_READ|PROT_EXEC, MAP_PRIVATE|MAP_DENYWRITE, 3, 0) = 0xfffffffff7556000    # map the executable text section of the library
... more stuff
# end of dynamic linker's code, finally jumps to our _start

gettimeofday({1461874556, 431117}, NULL) = 0
fstat64(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0  # stdio is figuring out whether stdout is a terminal or not
ioctl(1, SNDCTL_TMR_TIMEBASE or SNDRV_TIMER_IOCTL_NEXT_DEVICE or TCGETS, 0xff938870) = -1 ENOTTY (Inappropriate ioctl for device)
mmap2(NULL, 4096, PROT_READ|PROT_WRITE, MAP_PRIVATE|MAP_ANONYMOUS, -1, 0) = 0xfffffffff7743000      # 4k buffer for stdout
write(1, "Hello, World!\n%esp at startup = 0xff938fb0\n", 43) = 43
exit_group(0)                           = ?
+++ exited with 0 +++

如果我们使用了_exit(0),或者自己用int 0x80进行了sys_exit系统调用,那么write(2)就不会发生。当stdout被重定向到非tty时,默认为全缓冲(而不是行缓冲),因此write(2)仅在exit(3)中的fflush(3)作为其一部分被触发。没有重定向,使用包含换行符的字符串调用printf(3)将立即刷新。
根据stdout是否为终端而表现出不同的行为可能是可取的,但前提是你是有意这样做的,而不是错误地这样做。

2
使用-nostartfiles -static进行构建时,如果_C_运行时需要提前运行,则在许多环境中可能存在危险。对于简单的事情,您可能不会遇到问题,但即使是printf也可能成为问题。这就是为什么如果您打算静态使用glibc,则您的代码应该在程序启动时手动调用__libc_init_first__dl_tls_setup__libc_csu_init(按照这个顺序)。您可以通过使用像MUSL这样的_C_库来避免这种情况,它们在调用函数之前不需要初始化。它们专为静态链接而设计。 - Michael Petch
你可以使用动态链接_GLIBC_,因为共享对象将自动由动态链接器调用其初始化代码,并执行这些初始化调用。 - Michael Petch
1
@MichaelPetch:我在上面的评论中提到了这一点,如果你向右滚动...我不想让答案变得混乱,但也许应该更加突出。哦对了,也许我的评论是错误的,因为它只适用于动态链接和-nostartfiles。无论如何,我要走了,几个小时后再回来。 - Peter Cordes
如果您有任何好的想法来呈现这些信息,请编辑。否则,我会在某个时候处理它们。 - Peter Cordes
1
请注意。巧合的是,我发现我们之前分享过一个相关答案,链接为https://dev59.com/OpPfa4cB1Zd3GeqPHcFU。虽然当时是在减少代码大小的背景下讨论的,但我没有提到静态链接所需的初始化序列。 - Michael Petch
@MichaelPetch:这个回答里的内容是否太多了?我认为在漫谈大例子和其他东西之前,我已经把基本信息放在了前面。x86标签的维基链接了这个回答,用于在64位机器上构建32位代码,所以我想将新手引导到这个问题可能会澄清其他困惑,并且说明strace和ltrace,至少提到gdb。 - Peter Cordes

7
我正在学习x86汇编(在64位Ubuntu 18.04上),并遇到了类似的问题,这个问题与“从地基开始编程”第四章中的例子[http://savannah.nongnu.org/projects/pgubook/ ]相同。在搜索后,我发现以下两行代码可以组装和链接:
as power.s -o power.o --32  
ld power.o -o power -m elf_i386

这些告诉计算机你只是在32位环境下工作(尽管是64位架构)。

如果你想使用,那么请使用汇编语言行:

as --gstabs power.s -o power.o --32.

.code32 似乎是不必要的。
我尚未尝试过您的方式,但 GNU 汇编器(gas)似乎也可以使用以下命令:
.globl start
#(即,全局无需'a')。
此外,建议您保留原始代码中的注释,因为似乎在汇编中大量注释是推荐的。(即使您是唯一查看代码的人,如果几个月或几年后再次查看代码,这将使更容易理解您当时的想法。)
知道如何修改它以使用64位R*X和RBP、RSP寄存器会很好。

1
正确的,.code32 在这里是无用的,就像我在我的回答中解释的那样。而且,.globl 是正常的GAS指令。.global 是它的别名 - Peter Cordes

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