在 64 位系统上组装 32 位二进制文​​件(GNU 工具链)

Posted

技术标签:

【中文标题】在 64 位系统上组装 32 位二进制文​​件(GNU 工具链)【英文标题】:Assembling 32-bit binaries on a 64-bit system (GNU toolchain) 【发布时间】:2016-08-20 02:28:33 【问题描述】:

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

as power.s -o power.o

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

ld power.o -o power

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

分段错误(核心转储)

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

【问题讨论】:

你已经做了什么来解决这个问题?您是否尝试过使用调试器单步执行代码以查看崩溃的位置?你知道什么是分段错误吗? (与在 64 位系统上运行 32 位代码无关)。 .code32 不设置目标文件格式。汇编器将生成 32 位代码,但将其放入默认以 64 位代码运行的文件中。大卫指出了一个单独的问题,但修复后它仍然会崩溃。 (或者至少得到错误的答案。objdump -d power 以 CPU 解释指令的方式反汇编指令。)使用 gcc -m32 -static -nostartfiles power.S -o power @PeterCordes :我同意段错误的大问题是 32 位代码作为 64 位程序运行,但我不相信大卫的分析是正确的(我不相信有ret 的堆栈问题会导致段错误)。 2^3 + 5^2 的结果应该是 33(看起来是) 再看一遍,你是对的@MichaelPetch。今天早上我第一眼看错了 %ebp 引用。 @DavidHoelzer : 没问题,大卫,去过那里! 【参考方案1】:

TL:DR:使用gcc -m32 -static -nostdlib foo.S(或等效的 as 和 ld 选项)。 或者如果你没有定义自己的_start,只需gcc -m32 -no-pie foo.S

你可能需要安装gcc-multilib,如果你链接libc,或者你的发行版包/usr/lib32/libc.so/usr/lib32/libstdc++.so等等。但是如果你定义了自己的_start 并且不链接库,你就不需要库包,只需要一个支持32位进程和系统调用的内核。这包括大多数发行版,但不包括适用于 Linux v1 的 Windows 子系统。

不要使用.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

Documentation for nostdlib, -nostartfiles, and -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 最终运行时,并非所有寄存器都会被清零,因为动态链接器会在您的进程中运行代码。

但是,gcc -nostartfiles -static hello.S 将链接,但在运行时崩溃如果你调用 printf 或其他东西而不调用 glibc 的内部初始化函数。 (见 Michael Petch 的评论)。


当然,您可以将.c.S.o 文件的任意组合放在同一命令行中,以将它们全部链接到一个可执行文件中。如果您有任何 C,请不要忘记 -Og -Wall -Wextra:当问题是 C 中的简单问题(编译器可能会警告您)时,您不想调试您的 asm。

使用-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 的平台。 (但 Linux 示例不适用于 OS X,因为系统调用号不同,或者在 Windows 上,因为它甚至不使用 int 0x80 ABI。)


NASM/YASM

gcc 无法处理 NASM 语法。 (-masm=intel 更像是 MASM 而不是 NASM 语法,您需要 offset symbol 来立即获取地址)。当然,指令是不同的(例如.globl vs global)。

您可以使用nasmyasm 进行构建,然后如上所述将.ogccld 直接链接。

我使用包装脚本来避免重复键入具有三个不同扩展名的相同文件名。 (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 有几个原因,包括它默认制作 long-nops 而不是用许多单字节 nops 填充。这会导致混乱的反汇编输出,并且如果 nop 运行过,也会变慢。 (在 NASM 中,您必须使用 smartalign 宏包。)

但是,YASM 已经有一段时间没有维护了,只有 NASM 支持 AVX512;这些天我更经常只使用 NASM。


示例:使用 _start 中的 libc 函数的程序

# 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 的 init 函数。 (根据 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、the write(2) wouldn't have happened 让sys_exit 系统调用我们自己。将 stdout 重定向到非 tty 时,它默认为全缓冲(不是行缓冲),因此 write(2) 仅由 fflush(3) 触发,作为 exit(3) 的一部分。如果没有重定向,使用包含换行符的字符串调用 printf(3) 将立即刷新。

根据 stdout 是否为终端而采取不同的行为可能是可取的,但前提是您是故意而不是错误地这样做。

【讨论】:

使用 -nostartfiles -static 构建时,如果需要提前运行 C 运行时,在许多环境中可能会很危险。对于简单的事情,您可能不会有问题,但即使 printf 也可能成为问题。这就是为什么如果您打算静态使用glibc,您的代码应该调用__libc_init_first__dl_tls_setup__libc_csu_init,以便在程序启动时手动(按此顺序)调用。您可以通过使用像MUSL 这样的C 库来避免这种情况,这些库在调用函数之前不需要初始化。它们专为静态链接而设计 您可以摆脱 GLIBC 的动态链接,因为该共享对象的初始化代码将由执行这些初始化调用的动态链接器自动调用。 @MichaelPetch:我确实在该行上方的评论中提到过,如果您向右滚动...我不想弄乱答案,但也许应该更突出。哦,对了,也许我的评论是错误的,因为它只适用于 dynamic 链接和-nostartfiles。无论如何,gtg,几个小时后 bbl。 如果您对如何呈现该信息有任何好的想法,请编辑,否则我会在某个时候。 保重。巧合的是,我发现我们之前分享了一个与此相关的答案 ***.com/questions/35208824/… 。我忘记了它,虽然它是在减少代码大小的背景下,所以我从来没有提到静态链接工作所需的初始化序列。【参考方案2】:

我正在学习 x86 汇编(在 64 位 Ubuntu 18.04 上)并且遇到了类似的问题使用完全相同的示例(它来自 Programming From the Ground Up,在第 4 章 [http://savannah.nongnu.org/projects/pgubook/])。

四处寻找后,我发现以下两行组装并链接:

as power.s -o power.o --32  
ld power.o -o power -m elf_i386

这些告诉计算机您只在 32 位中工作(尽管是 64 位架构)。

如果要使用gdb debugging,则使用汇编行:

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

.code32 似乎是不必要的。

我没有按照你的方式尝试过,但是 gnu 汇编器(gas)似乎也可以: .globl 开始 #(即全局中没有'a')。

此外,我建议您可能希望保留原始代码中的 cmets,因为似乎建议您在汇编中大量注释。 (即使您是唯一一个查看代码的人,如果您在数月或数年后查看代码,也会更容易弄清楚您在做什么。)

很高兴知道如何更改它以使用 64-bit R*XRBPRSP 寄存器。

【讨论】:

正确,.code32 在这里没用,正如我在回答中解释的那样。是的,.globl is the normal GAS directive. .global is an alias for it。

以上是关于在 64 位系统上组装 32 位二进制文​​件(GNU 工具链)的主要内容,如果未能解决你的问题,请参考以下文章

无法在 64 位 Debian 上运行 32 位二进制文​​件

gcc:在 32 位平台上编译 64 位二进制文​​件

使用 g++ -march=x86-64 构建的代码可以在 32 位操作系统上运行吗?

在 64 位 Debian wheezy 多架构主机上编译使用 ssl 的 32 位二进制文​​件

为啥 32 位内核可以运行 64 位二进制文​​件?

在运行 64 位 linux 的 Armv8 (aarch64) 上编译并运行 32 位二进制文​​件