提问人:buweilv 提问时间:4/26/2016 最后编辑:d-cubedbuweilv 更新时间:11/7/2020 访问量:9727
在 64 位系统上组装 32 位二进制文件(GNU 工具链)
Assembling 32-bit binaries on a 64-bit system (GNU toolchain)
问:
我编写了成功编译的汇编代码:
as power.s -o power.o
但是,当我尝试链接对象文件时,它失败了:
ld power.o -o power
为了在 64 位操作系统 (Ubuntu 14.04) 上运行,我在文件开头添加了,但我仍然收到错误:.code32
power.s
分段故障(内核转储)
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,您可能需要安装 gcc-multilib
,或者您的发行版包,依此类推。但是,如果您定义了自己的库并且不链接库,则不需要库包,只需要一个支持 32 位进程和系统调用的内核。这包括大多数发行版,但不包括适用于 Linux v1 的 Windows 子系统。/usr/lib32/libc.so
/usr/lib32/libstdc++.so
_start
不要使用.code32
.code32
不会更改输出文件格式,这决定了程序的运行模式。您可以决定是否尝试在 64 位模式下运行 32 位代码。 用于组装具有一些 16 位和一些 32 位代码的内核,以及类似的东西。如果这不是您正在做的事情,请避免它,这样当您在错误模式下构建时,如果它有任何或说明,例如,您将收到构建时错误。 只是允许您创建令人困惑的调试运行时问题,而不是构建时错误。.code32
.S
push
pop
.code32
建议:使用手写汇编程序的扩展。( 将通过 C 预处理器运行它,因此您可以 例如,用于系统调用号码)。此外,它还将其与编译器输出 (from ) 区分开来。.S
gcc -c foo.S
as
#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
的文档。
使用 libc 函数 from (有关示例,请参阅本答案末尾)_start
某些函数(如 )或 stdio 函数(包括 )依赖于一些正在初始化的全局数据(例如 以及它实际指向的对象)。malloc(3)
printf(3)
FILE *stdout
gcc -nostartfiles
省略 CRT 样板代码,但仍保持链接(默认情况下是动态的)。在 Linux 上,共享库可以具有初始值设定项部分,这些部分在加载时由动态链接器运行,然后跳转到入口点。所以 gcc -nostartfiles 你好。S
仍然允许您调用 printf
。对于动态可执行文件,内核在其上运行,而不是直接运行(用于查看二进制文件中的“ELF 解释器”字符串)。最终运行时,并非所有寄存器都将归零,因为动态链接器在进程中运行了代码。_start
libc
_start
/lib/ld-linux.so.2
readelf -a
_start
但是,gcc -nostartfiles -static hello.S
将链接,但如果您调用或某些东西而不调用 glibc 的内部 init 函数,则会在运行时崩溃。(参见 Michael Petch 的评论)。printf
当然,您可以将 、 和文件的任意组合放在同一个命令行上,以将它们全部链接到一个可执行文件中。如果你有任何 C,别忘了:当问题在 C 中调用编译器可能已经警告过它时,你不想调试你的 asm。.c
.S
.o
-Og -Wall -Wextra
用于让 gcc 向您显示它运行以组装和链接的命令。要“手动”执行此操作:-v
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 的两个不同选项( 和 )更容易记忆和键入。此外,它适用于所有平台,包括可执行格式不是 ELF 的平台。(但 Linux 示例在 OS X 上不起作用,因为系统呼叫号码不同,或者在 Windows 上不起作用,因为它甚至不使用 ABI。--32
-m elf_i386
int 0x80
NASM/YASM公司
gcc 无法处理 NASM 语法。( 更像是 MASM 而不是 NASM 语法,您需要立即获取地址)。当然,指令是不同的(例如 与 )。-masm=intel
offset symbol
.globl
global
您可以使用 nasm
或 yasm
进行构建,然后按上述方式链接 with,也可以直接链接。.o
gcc
ld
我使用包装脚本来避免重复键入具有三个不同扩展名的相同文件名。(nasm 和 yasm 默认为 -> ,不像 GNU as 的默认输出 )。使用此功能可以组装和链接 32 位 ELF 可执行文件。并非所有操作系统都使用 ELF,因此此脚本的可移植性不如使用 to link 的可移植性。file.asm
file.o
a.out
-m32
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,包括它默认为长 s 而不是填充许多单字节 s。这会导致混乱的拆卸输出,并且如果 nops 运行,则速度会变慢。(在 NASM 中,您必须使用宏包。nop
nop
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 的评论。存在其他实现,包括 MUSL,它专为静态链接而设计,无需初始化调用即可工作。__libc_init_first
__dl_tls_setup
__libc_csu_init
libc
$ 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 _start
layout reg
run
$ 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 +++
如果我们使用 ,或者让系统调用自己,write(2)
就不会发生。将 stdout 重定向到非 tty 时,它默认为 full-buffered(而不是 line-buffered),因此 仅由 作为 的一部分触发。如果不进行重定向,则使用包含换行符的字符串进行调用将立即刷新。_exit(0)
sys_exit
int 0x80
write(2)
fflush(3)
exit(3)
printf(3)
根据 stdout 是否是终端而采取不同的行为可能是可取的,但前提是您是故意这样做的,而不是错误地这样做。
评论
-nostartfiles -static
glibc
__libc_init_first
__dl_tls_setup
__libc_csu_init
-nostartfiles
我正在学习 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*X
RBP
RSP
评论
.code32
不设置对象文件格式。汇编程序将生成 32 位代码,但将其放入默认为 64 位代码的文件中。David指出了一个单独的问题,但在修复该问题后它仍然会崩溃。(或者至少得到错误的答案。 以 CPU 解释指令的方式反汇编指令。用。objdump -d power
gcc -m32 -static -nostartfiles power.S -o power
ret