一般来说编译汇编代码(即汇编助记符)使用最多的工具为gas(即GNU as)和nasm,但用gas编译汇编代码给我留下了不太美好的回忆,所以这里只记录用nasm编译手写的汇编代码并运行的流程。
预备知识
我们从手写的汇编代码到最后实际运行,不仅需要把汇编代码编译为机器码,还需要转化为可执行文件的格式,文件格式默认为elf。elf可执行文件的程序入口点(ENTRY)一般是符号_start
,也就是由elf头中的e_entry
域决定的。使用_start
作为elf文件的入口点只是约定俗称,我们可以自己写linker的链接脚本来决定使用什么符号作为入口点。默认的链接脚本可以通过ld
来查看:
$ ld --verbose
...
/* Script for -z combreloc -z separate-code */
/* Copyright (C) 2014-2023 Free Software Foundation, Inc.
Copying and distribution of this script, with or without modification,
are permitted in any medium without royalty provided the copyright
notice and this notice are preserved. */
OUTPUT_FORMAT("elf64-x86-64", "elf64-x86-64",
"elf64-x86-64")
OUTPUT_ARCH(i386:x86-64)
ENTRY(_start)
SEARCH_DIR("/usr/x86_64-pc-linux-gnu/lib64"); SEARCH_DIR("/usr/lib"); SEARCH_DIR("/usr/local/lib"); SEARCH_DIR("/usr/x86_64-pc
-linux-gnu/lib");
SECTIONS
{
PROVIDE (__executable_start = SEGMENT_START("text-segment", 0x400000)); . = SEGMENT_START("text-segment", 0x400000) + SIZEOF
_HEADERS;
...
可以看到,ENTRY(_start)
决定了elf入口点使用的符号。我们实际写汇编代码时不一定要使用_start
符号作为函数的标签名,虽然链接成可执行文件时linker会告诉你无法找到符号_start
就是了,但这并不会影响程序的运行,没有_start
符号时程序默认使用0x401000即.text
段的开头作为入口点。
nasm汇编
本文不会提及关于x86汇编的知识,这里我们只指出关于使用nasm汇编的一些必要知识。
首先是注释,nasm使用;
作为注释的开头,一些汇编器或者语法也会使用#
作为注释的标记。其次是代码段的定义,我们常用的有.text
,.data
和.bss
段等,这些代码段与程序实际运行时一致,例如.text
段的读写权限为r-x
,在内存中存放实际运行的机器码。最后是常量的定义,多说无益,直接上一段容易理解的实例:
; Sections:
section .data
hello: db "Hello !", 10
len_of_hello equ $−hello
section .bss
input: resb 16
section .text
global _start
; Functions
_start:
mov rax, 60
mov rdi, 0
syscall
section
用于定义程序的.data
和.text
段,global
与extern
相对立,根据nasmdoc所描述的:
The GLOBAL directive applying to a symbol must appear before the definition of the symbol.
即,在写汇编函数前需要先用global声明,但这也不影响最后linker链接生成的elf文件的执行(linker同样会告诉你无法找到符号_start
),只要不作死把section .text
这段声明去掉就行。
.data
段中,nasm使用db
来定义数据,db
的使用比较灵活,既可以定义多个字节也可以定义字符串,nasmdoc没有去描述db的细节(或者说只是我没找到)。与db
作用类似的命令还有dw
,dd
等,秉着多一事不如少一事的原则,db
已经足够应对大部分场景,下面是nasmdoc中的几个例子:
db 0x55 ; just the byte 0x55
db 0x55,0x56,0x57 ; three bytes in succession
db ’a’,0x55 ; character constants are OK
db ’hello’,13,10,’$’ ; so are string constants
equ
也可以用于定义数据,关于db
和equ
的区别,这篇文章给了一个很好的解释,equ
对应C中的宏定义,db
对应C中的变量声明。除此之后值得注意的一个点是$
符号的使用,nasmdoc同样给出了说明:
NASM supports two special tokens in expressions, allowing calculations to involve the current assembly position: the
$ tokens. $ evaluates to the assembly position at the beginning of the line containing the expression; so you can code an infinite loop using JMP $. $$ evaluates to the beginning of the current section; so you can tell how far into the section you are by using ($−$$).
$
用于标识当前这行汇编指令的起始位置,$$
用于标识当前程序段的起始位置,$-hello
表示当前位置减去hello
符号的位置,即字符串的长度。
而在.bss
段中,nasm使用resb
,resw
和resq
定义未初始化的数据,nasmdoc同样也举了例子:
buffer: resb 64 ; reserve 64 bytes
wordvar: resw 1 ; reserve a word
realarray: resq 10 ; array of ten reals
nasmdoc同样只有举例没有说明,不过这不算很重要,目前够用就行。
编译与链接
将刚刚的汇编代码保存为名为test.s
的文本文件,我们只需要两行命令就能生成elf可执行文件:
nasm -f elf64 test.s # f option refers to format of output
ld test.o -o test # use linker `ld` to generate executable
如此,我们生成了一个.o
目标文件和格式为64为elf的可执行文件test
,这时可能就有人要问了,你这手写汇编也没有调libc和外部函数,为什么非要链接一遍?答案解释起来比较麻烦,我的建议是参考这个回答,简单来说C相当于就是高级汇编,编译C要过一遍的事情用汇编写同样要过一遍,所以链接在大部分场景也是必须的。那如果一定要用一行命令把汇编代码编译成可执行文件,我只能推荐gcc
给出的解决方案,但是写汇编的话就需要按照as
的方式来写:
gcc -nostdlib -static -no-pie start.s -o static_executable
以上就是用nasm手写汇编的全部内容,更多的细节这里不会补充,有兴趣可以跑一下以下程序:
section .data
hello: db "Hello Assembly!", 10
len_of_hello equ $-hello
non_exist: db "I am void"
section .text
global _start
_start:
mov eax, 2
mov rdi, non_exist
xor esi, esi
xor edx, edx
syscall
mov eax, 1
mov rdi, 1
mov rsi, hello
mov rdx, len_of_hello
syscall
References
- https://stackoverflow.com/questions/4272316/in-an-elf-file-how-does-the-address-for-start-get-detemined
- https://stackoverflow.com/questions/70009183/if-an-object-file-defines-start-and-doesnt-use-any-libraries-why-do-i-still-n
- How programs get run - part one
- How programs get run - part two
- The Basics Of Writing Assembly
- What's the difference between equ and db in NASM?