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
时获得构建时错误,例如如果有任何
push
或
pop
指令。使用
.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
gcc -g foo.S -o foo -m32 -no-pie
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 &&
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
来获取地址作为立即数。)当然,指令也是不同的(例如.globl
与global
)。
您可以使用nasm
或yasm
进行构建,然后像上面一样使用gcc
链接.o
,或直接使用ld
。
我使用一个包装脚本来避免重复键入相同文件名的三个不同扩展名。(nasm和yasm默认生成file.asm
-> file.o
, 而不像GNU as的默认输出a.out
)。搭配-m32
使用可以汇编和链接 32位 ELF 可执行文件。并非所有操作系统都使用 ELF,所以这个脚本比使用gcc -nostdlib -m32
进行链接要不太可移植。
#!/bin/bash
verbose=1
fmt=-felf64
ldlib=()
linker=ld
while getopts 'Gdsphl:m:nvqzN' opt; do
case "$opt" in
m) if [ "m$OPTARG" = "m32" ]; then
fmt=-felf32
ldopt=-melf_i386
fi
if [ "m$OPTARG" = "mx32" ]; then
fmt=-felfx32
ldopt=-melf32_x86_64
fi
;;
l) linker="gcc -no-pie -fno-plt -nostartfiles"; ldlib+=("-l$OPTARG");;
p) linker="gcc -pie -fno-plt -nostartfiles"; ldlib+=("-pie");;
h) ldlib+=("-Ttext=0x200800000");;
G) nodebug=1;;
d) disas=1;;
s) runsize=1;;
n) use_nasm=1 ;;
q) verbose=0 ;;
v) verbose=1 ;;
z) ldlib+=("-zexecstack") ;;
N) ldlib+=("-N") ;;
esac
done
shift "$((OPTIND-1))"
src=$1
base=${src%.*}
shift
set -e
if (($use_nasm)); then
( (($verbose)) && set -x
nasm "$fmt" -Worphan-labels $dbg "$src" "$@" &&
$linker $ldopt -o "$base" "$base.o" "${ldlib[@]}")
else
(($nodebug)) || dbg="-gdwarf2"
( (($verbose)) && set -x
yasm "$fmt" -Worphan-labels $dbg "$src" "$@" &&
$linker $ldopt -o "$base" "$base.o" "${ldlib[@]}" )
fi
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
$ 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
$ 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
exit(0 <no return ...>
+++ exited (status 0) +++
$ strace -s128 ./a.out > /dev/null
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
... more stuff
gettimeofday({1461874556, 431117}, NULL) = 0
fstat64(1, {st_mode=S_IFCHR|0666, st_rdev=makedev(1, 3), ...}) = 0
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
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是否为终端而表现出不同的行为可能是可取的,但前提是你是有意这样做的,而不是错误地这样做。
ret
存在堆栈问题会导致segfault)。2^3 + 5^2的结果应该是33(它似乎是这样的)。 - Michael Petchgcc -m32 -static -nostartfiles power.S -o power
然后我成功地运行了power。-m32
选项允许在x86-64平台上生成32位代码。然而,我不知道为什么要使用-static
选项来静态链接程序。我也没有找到-nostartfiles
选项的含义。@PeterCordes - buweilv