看一看一些字符串与字符串操作。
字符串反转(Reverse string)
既然谈论汇编语言,当然不能遗漏__字符串(string)__数据类型,通常来说,它就是我们使用单位字节长度的数组来构建字符串数据类型。写一些简单的栗子:我们将会定义字符串数据,反转他们后输出。这个例子非常简单,但也很有用,尤其学习一门新语言的时候。
1 | section .data |
四个常量:
- SYS_WRITE – ‘write’ syscall number
- STD_OUT – 标准输出文件描述符(stdout file descriptor)
- SYS_EXIT – ‘exit’ syscall number
- EXIT_CODE – exit code
定义了一些变量: - NEW_LINE – 换行符号(\n)
- INPUT – 输入字符串
接下来为缓存(buffer)定义 bss 段,这里将会放入被反转的字符串:
1 | section .bss |
现在有了存放数据与缓存的地方,那么轮到放代码的 text 段啦。以 _start 开始吧:
1 | _start: |
这里是一些新的东西。看看他们怎么工作:在第二行先把 INPUT 的地址放入 si 寄存器。为了输出字符串,rcx 赋值为 0,它是计算字符串长度的计数器。第四行见到了 cld 操作符。它重置了 df 标志,使之为 0。因为稍后需要利用它计算字符串长度 – 会遍历字符串中的字符,当 df 标志为 0 时,会从左到右处理字符。接下来调用了 calculateStrLength 函数。对了,遗忘了第五行的mov rdi, $ + 15
,不用着急,会在稍后提到。先看一看 calculateStrLength:
1 | calculateStrLength: |
正如它的名字,这函数的功能是计算字符串的长度并把长度存储到 rcx。首先检查 rsi 是否为 0(NULL),若为 0 则意味着计算结束。接下来是 lodsb,它仅仅把 1 字节放入了 al 寄存器(16 位寄存器 ax 的低 8 位),并改变了 rsi 指针。当执行 cld 指令后,每次执行 lodsb 都会把 rsi 从左至右移动一字节(译者注:x86 体系是小端存储),这样便可以移动字符了。之后,把 rax 放入堆栈,这样堆栈中便包含了字符串中的一个字符(lodsb 把一字节从 si 放入了 al,al 为 rax 的低 8 位)。我们怎能把字符放入堆栈呢?这必须得记得堆栈是怎么工作的:它的工作原理为 LIFO(后入先出)。我们把字符依次从 si 放入堆栈。这样,最后一个字符已定在堆栈的顶端。然后仅仅需要从堆栈从弹出(pop)字符,再写入 OUTPUT 缓存就行了。push rax
后,自增了计数器(rcx),循环执行这段代码。
把所有的字符放入堆栈后,跳转到 exitFromRoutine,再返回 _start。怎么做呢?有 ret 指令:
1 | exitFromRoutine: |
但是它并不会工作。为啥?这很诡异。要知道我们的确在 _start 中调用了 calculateStrLength.但是在调用的过程中究竟发生了什么呢?起初,所有函数参数从右至左放入了堆栈,之后返回了地址,它也放入了堆栈。因此函数在调用结束后知道从哪里返回。但是看一下 calculateStrLength,我们把符字符串中的字符都放入了堆栈,现在的堆栈顶部并不是要返回的地址,函数调用结束后当然不知道从哪里返回了。现在怎么办呢?先看一看前面被忽略的诡异的指令吧:
1 | mov rdi, $ + 15 |
开始前:
- $ – 返回一地址,这地址是这条汇编语句在内存中的位置
- $$ – 也返回一地址,这地址为当前段(section)的起点
利用mov rdi, $ + 15
我们便得到了一个地址,但为啥加了 15?我们需要知道 calculateStrLength 的下一条语句的地址。现在看一看使用 objdump(译者注:反汇编工具) 查看文件后的结果吧:
1 | $objdump -D reverse |
瞧,第十二行(mov rdi, $ + 15
)的命令占用了 10 字节(译者注:C8h-BEh=10),第 16 行调用函数占用了 5 字节(译者注:CDh-C8h=5),加在一起不就是 15 字节?这地址就是我们需要的返回地址。这样便可以把 rdi 的值抛进堆栈,再从函数中返回至 _start:
1 | exitFromRoutine: |
调用 calculateStrLength 后,rax 和 rdi 都写入了 0,之后跳转到 reverseStr。它是这样的:
1 | reverseStr: |
这里我们检查了我们的字符串计数器,若为零则表示我们已经把所有的字符写入了缓存,现在可以打印了。不为零就从堆栈中弹出字符,放入 rax,再写入 OUTPUT 缓存。之后自增 rdi ,移动到 OUTPUT 缓存的下一个位置,同时自字符串长度计数器自减,程序跳转到开始处。
执行完 reverseStr 后,就已经把字符串反转了,反转后的字符串存放在 OUTPUT 缓存中。该用新的一行输出他们了:
1 | printResult: |
这样退出:
1 | exit: |
就这么多,现在可以编译我们的程序了:
all:
1 | nasm -g -f elf64 -o reverse.o reverse.asm |
译者注:Makefile, GNU Make 工具使用的文件。
这是执行结果:
字符串操作(String operations)
肯定有许多其他的字符串/比特(string/bytes)相关的指令操作啦:
- REP – 当 rcx 不为零时重复执行
- MOVSB – 拷贝一比特大小的字符(MOVSW, MOVSD 等)
- CMPSB – 比特大小的字符比较
- SCASB – 比特大小的字符输入
- STOSB – 存储一比特大小的字符至某寄存器
结语
此系列第四篇。下一节谈论 nasm 的宏(macroses)。
原文 / 源码
译者注:
看不懂/忘记一些命令?尤其是 REP / MOVSB 这些?为何不 Google 之?Intel Assemble Instruction Set 的确是一个好地方。