在64位系统(GNU工具链)上组装32位二进制文件
我编写可以编译的汇编代码:
as power.s -o power.o
链接power.o目标文件时出现问题:
ld power.o -o power
为了在64位操作系统(Ubuntu 14.04)上运行,我在power.s
文件的开头添加了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
。
.code32
不会改变输出文件的格式,这就决定了你的程序运行的模式。不要试图在64位模式下运行32位代码。 .code32
用于汇编您可能想要的数据的“外部”机器代码,或导出到共享内存段中。 如果这不是你正在做的事情,那么避免它,所以当你在错误的模式下构build一个.S
,如果它有任何push
或pop
指令的话,你会得到构build时的错误。
build议:使用手写汇编器的.S
扩展名。 ( gcc foo.S
会在as
之前通过C预处理器来运行它,所以你可以#include
一个包含系统调用号的头文件)。 此外,它与.s
编译器输出(来自gcc foo.c -O3 -S
)区分开来。
要构build32位二进制文件,请使用这些命令之一
gcc -g foo.S -o foo -m32 -nostdlib -static # static binary with absolutely no libraries or startup code # -nostdlib by itself makes static executables on Linux, but not OS X. gcc -g foo.S -o foo -m32 # dynamic binary including the startup boilerplate code. Use with code that defines a main() but not a _start
nostdlib
, -nostartfiles
和-static
文档 。
使用_start
libc函数(参见本答案的结尾部分)
一些函数,如malloc(3)
,或者stdio函数(包括printf(3)
依赖于一些全局数据的初始化(例如FILE *stdout
和它实际指向的对象)。
gcc -nostartfiles
省略了CRT _start
样板代码,但仍然链接libc
(默认情况下是dynamic的)。 在Linux上,共享库可以具有初始化器部分,在加载它们之前由dynamic链接器运行,然后跳转到_start
入口点。 所以gcc -nostartfiles hello.S
仍然让你打电话给printf
。 对于dynamic可执行文件,内核运行/lib/ld-linux.so.2
,而不是直接运行(使用readelf -a
来查看二进制文件中的“ELF interpreter”string)。 当_start
最终运行时,并不是所有的寄存器都会被清零,因为dynamic链接器在你的进程中运行代码。
但是, gcc -nostartfiles -static hello.S
会链接,但是如果在调用glibc的内部初始化函数时调用printf
或其他东西,会在运行时崩溃 。 (见Michael Petch的评论)。
当然,您可以将.c
, .S
和.o
文件的任意组合放在同一个命令行上,将它们全部链接到一个可执行文件中。 如果你有任何C,不要忘了-Og -Wall -Wextra
:当C中的问题是简单的,你不想debugging你的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
)的两个不同的选项更容易记住和input。 此外,它适用于所有平台,包括可执行格式不是ELF的平台。 ( 但是Linux的例子不能在OS X上工作,因为系统调用号码是不同的 ,或者在Windows上,因为它甚至不使用int 0x80
ABI。)
NASM / YASM
gcc无法处理NASM语法。 ( -masm=intel
比MASM更像NASM语法,在这里你需要offset symbol
来获取地址作为立即数)。 当然,这些指令是不同的(例如.globl
vs global
)。
你可以使用nasm
或者yasm
来构build,然后像上面那样将.o
和gcc
链接起来,或者直接使用ld
。
我使用包装脚本来避免用三种不同的扩展名重复input相同的文件名。 (nasm和yasm默认为file.asm
– > file.o
,而不像GNU的默认输出a.out
)。 将其与-m32
一起使用来汇编和链接32位ELF可执行文件。 并不是所有的操作系统都使用ELF,所以这个脚本比使用gcc -nostdlib -m32
来连接的gcc -nostdlib -m32
要低。
#!/bin/sh # 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 verbose=1 # defaults fmt=-felf64 #ldopt=-melf_i386 while getopts 'm:vq' 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 # default is -m64 ;; q) verbose=0 ;; v) verbose=1 ;; esac done shift "$((OPTIND-1))" # Shift off the options and optional -- src=$1 base=${src%.*} shift [ "$verbose" = 1 ] && set -x # print commands as they're run, like make #yasm "$fmt" -Worphan-labels -gdwarf2 "$src" "$@" && nasm "$fmt" -Worphan-labels -g -Fdwarf "$src" "$@" && ld $ldopt -o "$base" "$base.o" # 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
我更喜欢yasm有几个原因,包括它默认使用long- nop
而不是填充许多单字节的nop
。 这会导致混乱的反汇编输出,以及如果nops运行速度变慢。 (在NASM中,您必须使用smartalign
macros包。)
示例:_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初始化函数。 ( __libc_init_first
, __dl_tls_setup
和__libc_csu_init
按照Michael Petch的说法,其他libc
实现也存在,包括为静态链接devise的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 _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 +++
如果我们使用_exit(0)
,或者让sys_exit
系统调用我们自己的int 0x80
, write(2)
就不会发生 。 将stdoutredirect到非tty时,默认为全缓冲(非线性缓冲),所以write(2)
仅作为exit(3)
一部分由fflush(3)
触发。 没有redirect,用包含换行符的string调用printf(3)
将立即刷新。
根据标准输出是否为terminal而有所不同,但只有在有目的的情况下才会这样做,而不是错误的。