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



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

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

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

如果链接libc,或者您的发行版包/usr/lib32/libc.so/usr/lib32/libstdc++.so等等,您可能需要安装gcc-multilib。但是,如果您定义了自己的_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

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最终运行时,并不是所有寄存器都将为零,因为动态链接器在您的进程中运行了代码。

但是,gcc -nostartfiles -static hello.S将链接,但如果您在不调用glibc的内部init函数的情况下调用printf或其他函数,则会在运行时崩溃。(见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 0x80ABI。)


NASM/YASM

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

您可以使用nasmyasm构建,然后如上所述将.ogcc链接,或者直接链接ld

我使用包装脚本来避免重复键入具有三个不同扩展名的相同文件名。(nasm和yasm默认为file.asm->file.o,与GNU的默认输出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有几个原因,包括它默认为长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 = %#lxn"

$ 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.outb _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 = %#lxn", 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 = 0xff938fb0n", 43) = 43
exit_group(0)                           = ?
+++ exited with 0 +++

如果我们使用_exit(0),或者让sys_exit系统用int 0x80调用我们自己,write(2)就不会发生。当stdout重定向到非tty时,它默认为全缓冲(而非行缓冲),因此write(2)仅由作为exit(3)一部分的fflush(3)触发。如果没有重定向,使用包含换行符的字符串调用printf(3)将立即刷新。

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

我正在学习x86汇编(在64位Ubuntu 18.04上),并在使用完全相同的示例时遇到了类似的问题(来自第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-start
#(也就是说,全局中没有"a")。

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

如果知道如何将其更改为使用64-bit R*XRBPRSP寄存器,那就太好了。

最新更新