稽古 (基于x86架构的)汇编语言浅试

Author: sandyzikun

如题.

Intro

操作不同架构的处理器的语言不同, 其统称为 汇编语言 (Assembly Language).
Intel, AMD 等厂家的处理器为x86架构, Apple与树莓派等为arm架构, 此外还有其他类型处理器与相应架构, 于此我们讨论面向x86处理器的汇编语言.

就算是面向同一架构的汇编语言, 也有不同语法之分, 例如面向x86架构的汇编语言也有AT&TIntel两种语法:

X86 Assembly
1
2
mov     $0x6,   %edi    ; AT&T
mov edi, 0x6 ; Intel

可以看出, AT&T语法中源操作数在目标操作数之前, 而Intel正好相反, 于此我们使用Intel语法.

Compiler

面向各种架构的任何汇编语言都是非常典型的编译式语言, 需要 编译器 (Compiler)源程式 (Source) 编译为 目标代码 (Object Code), 再连接为可执行文件.

面向x86架构的汇编器有Microsoftmasm, Borlandtasm, 而我们今天将要用到的是nasm (Netwide Assembler)10, 它是一个最初是在 Julian Hall 的协助下由 Simon Tatham 开发的, 目前由 Hans Peter Anvin 领导的小团队所维护的 2-clause BSD License 开源项目11.

Debian系的Linux中可以直接通过apt进行安装:

Shell
1
$ apt install nasm

Structure

asm (代指汇编语言) 的结构分为 段落 (Section) 与其他部分 (这些我们暂且称为过程 (Routine), 因为很多人都这么叫):

  1. Section .text: 用于宣告主程式入口的部分, 一般会宣告为_start:

    X86 Assembly
    1
    2
    section .text
    global _start
  2. Section .bss: 全称为 Block Starting Symbol, 其包含整个程式周期中变化的变量们, 可认为是宣告变量名的部分, 一般格式为:

    X86 Assembly
    1
    2
    3
    section .bss
    <name> <type> <content>
    ... ; Other Variables

    每行宣告一个变量, <name>处为变量名, <type>为类型, <content>为初始内容, 字节类型的变量可以宣告类型为 resb (Reserve Byte), 于是<name>处的名称便是表示这个变量起始地址的标识, 可以类比C/C++中的 指针 (Pointer), 具体写法可以参考如下:

    X86 Assembly
    1
    2
    section .bss
    inst resb 16
  3. Section .data: 用于宣告程式中不变的数据, 可以视为常量, 宣告形式与.bss中的变量类似, 但是对字节数据的宣告类型为 db (Define Bytes):

    X86 Assembly
    1
    2
    3
    section .data
    <name> <type> <content>
    ... ; Other Constants

    For Instance:

    X86 Assembly
    1
    2
    3
    4
    5
    6
    section .data
    txt1 db "Sharing the World", 0xa, 0
    txt2 db "We're Sharing an Endless Love", 0xa, 0
    txt3 db "Sharing our World", 0xa, 0
    txt4 db "Sharing your Sound", 0xa, 0
    txt5 db "Connecting our Feelings", 0xa, 0
  4. 目前看来, 64位的asm的主过程必须以_start为标识符, 例如:

    X86 Assembly
    1
    2
    _start:
    ... ; do something

Operations

在笔者个人看来, asm实质上是某种函数式执行, 即把数据置于特定的寄存器, 处理器内部的逻辑电路以其方式读取这些寄存器中的数据, 执行相应的操作:

X86 Assembly
1
2
3
4
(_start)
mov rax, 60
mov rdi, 0
syscall

mov指令即move, 但其行为是把源寄存器中的内容向目标寄存器复制, 在这里我们便是设置 rax=60, rdi=0.
syscall 命令用于根据寄存器中当前存储的值调用相应的系统函数, 在此我们调用的是 sys_exit, 这是表示在程式结束之时退出程式的功能.

这里我们给出一个列表, 表示不同系统函数调用时应向寄存器中存储的值:

syscall rax rdx rdi rsi
sys_read 0 读取的最大字节数 0 (表示写入) 表示写入字节的地址的寄存器
sys_print 1 输出的最大字节数 1 (表示输出) 表示输出字节源地址的寄存器
sys_exit 60 可认为是整个程式的返回值, 一般为0, 表示正常结束

Sub Routine

子过程, 用于程式分解与复用:

X86 Assembly
1
2
3
4
5
6
7
8
9
10
11
_start:
call _printext
... ; do something

_printext:
mov rax, 1
mov rdi, 1
mov rsi, txt1
mov rdx, 18
syscall
ret

这里可以看出来_printext部分输出了txt1指向的前18格字节, 并通过call命令在主程式中进行了调用.
但如若最后不使用ret返回, 则会向下执行后面的程式块, 这一点与C/C++中的goto跳转以及switch-case结构相似.

Macro

计算机科学里的 宏 (Macro) 是一种抽象, 它根据一系列预定义的规则替换一定的文本模式.

在asm中我们可以认为macro是一种比sub-routine更为泛化更为抽象的代码复用机制, 在x86的asm中具体形式如下:

X86 Assembly
1
2
3
%macro  <name>  <num_parameters>
... ; do something
%endmacro

譬如我们定义一个接收一个参数的macro, 用于程式结束:

X86 Assembly
1
2
3
4
5
%macro  exit    1
mov rax, 60
mov rdi, %1
syscall
%endmacro

%1接收的便是提供的第一个参数, 譬如我们使用

X86 Assembly
1
2
3
_start:             ; or any other Routine
... ; do something
exit 0

便等效于以下代码:

X86 Assembly
1
2
3
4
5
_start:             ; or any other Routine
... ; do something
mov rax, 60
mov rdi, 0
syscall

Program

于此展示笔者根据视频教程2编写的一份示例:

X86 Assemblyinit-01.asm
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
section .data
txt0 db 0xa, 0
txt1 db "Sharing", 0x20, 0
txt db "Holax, Sekai!", 0xa, 0
txt2 db "We're Sharing an Endless Love", 0xa, 0
dig db 0

section .bss
inst resb 16

section .text
global _start

%macro exit 1 ; Declaring the Name and the Num of Parameters of the Macro
mov rax, 60 ; Declaring the Function we're to call is `sys_exit`
mov rdi, %1 ; `%[k]` means the k-th Parameter
syscall ; Calling the Function `sys_exit`
%endmacro

_start:
call _printext
mov rax, 3
call _printdgrax
mov rax, 9
call _printdgrax
mov rax, txt0
call _printstrax
mov rax, txt1
call _printstrax
call _readstr
mov rax, txt2
call _printstrax
exit 0 ; Process of exiting the Program

_printext: ; A Sub-routine calling the Function `sys_write` to print the Bytes to which `rax` pointed
mov rax, 1 ; Declaring the Function we're to call is `sys_read`
mov rdi, 1
mov rsi, txt
mov rdx, 14
syscall
ret

_readstr: ; A Sub-routine calling the Function `sys_read`(?) to read the String which the User inputs then stored it in `inst`
mov rax, 0 ; Declaring the Function we're to call is `sys_read`
mov rdi, 0
mov rsi, inst
mov rdx, 16
syscall
ret

_printdgrax:
add rax, 48 ; Add 48 to the Value of `rax`, which refers to the ascii of any digit
mov [dig], al ; Temporarily storing the Byte of the Digit in `[dig]` from `al`
mov rax, 1
mov rdi, 1
mov rsi, dig
mov rdx, 1
syscall
ret

;input: rax as a pointer to the specified string
;output: print string as rax
_printstrax:
push rax ; Pushing the Value of `rax` into the Stack
mov rbx, 0 ; Setting the Value of `rbx` to zero, for counting Length of the Bytes later
_prloop:
inc rax ; ++`rax`, which makes it point to the Byte next to the one pointed to previously
inc rbx ; ++`rbx`, which means the Length it recorded is added by one
mov ch, [rax] ; Temporarily storing the Byte in `cl` (or using `ch` is also proper), which `rax` pointed to, for being compared to zero later
cmp ch, 0 ; Compare the Byte to zero,
jne _prloop ; * if the result is not equal, go back to `_prloop` (`jne` == "Jump when Not Equal")
; * otherwise, execute the program below:
mov rax, 1 ; Declaring the Function we're to call is `sys_write`, like every time we called it previously
mov rdi, 1 ;
pop rsi ; The top of the Stack should be pushed from `rax` previously, which means it refers to the Starter of the String
; Now we pop it to `rsi`
mov rdx, rbx ; (Actually we can also operate on `rdx` directly only in this case, nevertheless operating on `rbx` before copying to `rdx` is more of safety
syscall
ret

执行以下命令即可编译运行:

Shell
1
2
3
4
5
6
7
$ nasm ./init-01.asm -f elf64
$ ld ./init-01.o
$ ./a.out
Holax, sekai!
39
Sharing (the World)
We're Sharing an Endless Love

(括号里面的内容the World是用户在终端里面的输入)

References

1. 100秒了解汇编语言 Bilibili: av213755649
2. 一套 YouTube 上很受欢迎的汇编语言教程, 在 Bilibili 上的搬运: 3 4 5 6 7 8 9
3. [英语字幕]NASM linux 汇编语言-1 x86_64 Linux Assembly #1 - ‘Hello, World!’ Bilibili: av672911046
4. [英语字幕]NASM linux汇编语言 - 2 x86_64 Linux Assembly #2 - “Hello, World!” Breakdown Bilibili: av757892807
5. [英语字幕]NASM linux汇编语言 -3 x86_64 Linux Assembly #3 - Jumps, Calls, Comparisons Bilibili: av972925372
6. [英语字幕]NASM linux汇编语言 -4 x86_64 Linux Assembly #4 - Getting User Input Bilibili: av587897493
7. [英语字幕]NASM linux汇编语言 -5 x86_64 Linux Assembly #5 - Math Operations and the Stack Bilibili: av845445104
8. [英语字幕]NASM linux汇编语言 -6 x86_64 Linux Assembly #6 - Subroutine to Print Strings Bilibili: av757891011
9. [英语字幕]NASM linux汇编语言 -7 x86_64 Linux Assembly #7 - Macros Bilibili: av930512566
10. NASM
11. 百度百科词条 NASM