之前的几篇文章简要地概述在x86_64汇编编程。你可以通过asm标记找到这些文章。在这边文章中我们将继续之前的文章。
这篇文章主要简单介绍了字符串操作,在这里仍然使用linux x86_64上的nasm汇编器。
字符串反转
在之前的文章中我们讨论的汇编编程语言并没有谈及字符串数据类型,实际上我们使用了一个字节数组。这里我们将编写一个简单的例子,主要定义了一个字符串类型的数据,将这个字符串反转并且从标准输出中打印出来。这个例子对于一个汇编新手来说的确很简单,让我们开始我们的字符串探索之旅吧。
首先,我们需要初始化数据。在数据节我们先定义数据:
section .data
SYS_WRITE equ 1
STD_OUT equ 1
SYS_EXIT equ 60
EXIT_CODE equ 0
NEW_LINE db 0xa
INPUT db "Hello world!"
在这里,我们定义了4个常量:
- SYS_WRITE - 'write'系统调用编号
- STD_OUT - stdout文件描述符
- SYS_EXIT - 'exit'系统调用编号
- EXIT_CODE - 退出码
同样也定义了一些其他常量:
- NEW_LINE - 换行符
- INPUT 需要被反转的字符串
下一步,我们在bss段中定义一个缓冲区,用于存放反转的字符串:
section .bss
OUTPUT resb 12
好了,我们定义完所需要的数据和存放结果的缓冲区后,现在我们开始编写代码,起点 _start:
_start:
mov rsi, INPUT
xor rcx, rcx
cld
mov rdi, $ + 15
call calculateStrLength
xor rax, rax
xor rdi, rdi
jmp reverseStr
这里我们发现了一些新的用法。让我们看看它是如何工作的:首先在第二行我们将INPUT的地址放入的RSI寄存器中,并且将RCX寄存器清零,RCX寄存器用于保存我们定义字符串的长度。第四行我们看到了一个cld指令,它重置DF标志为0。在字符串的比较,赋值,读取等一系列和rep连用的操作中,di或si是可以自动增减的而不需要人来加减它的值,cld即告诉程序si,di向前移动,std指令为设置方向,告诉程序si,di向后移动。将DF标志为0后,我们就可以知道,字符串从左到右移动。
接着,我们调用calculateStrLength函数,从它的命名我们也知道它是做什么的。这里第五行mov rdi, $ + 15并没有解释它的含义,后面会详细地描述它。现在先看看calculateStrLength的实现:
calculateStrLength:
;; check is it end of string
cmp byte [rsi], 0
;; if yes exit from function
je exitFromRoutine
;; load byte from rsi to al and inc rsi
lodsb
;; push symbol to stack
push rax
;; increase counter
inc rcx
;; loop again
jmp calculateStrLength
从它的命名上我们知道它是计算INPUT字符串长度的,然后将值放在RCX寄存器中。首先我们检查RSI就存器没有指向0,如果指向0说明这里就是字符串的结束位置,然后从函数返回。下一步,使用lodsb指令,这个指令将RSI指向的1个字节放到al寄存器(rax寄存器低8位)中,并且增加RSI指针到下一个字节。就像我们前面所说的cld指令指示lodsb指令每一次移动RSI从左到右加载指向的内存地址。然后,我们将这个rax中的值压入栈中,这样执行完后,我们在栈中就保存了我们的字符串。也许您会问为什么将其压入栈中?您肯定还记得之前描述的栈怎么工作的,栈是一个后进先出(LIFO)的队列,它非常应用我们这里的场景。我们先将字符串的起始字符压入栈,执行完后,栈顶将会是最后一个字符。然后我们简单的将符号一个个弹出到OUTPUT缓冲区中就得到了反转的字符串。
好了,我们将字符串中的字符全部压入栈了,现在我们到exitFromRoutine中返回到_start,怎么做呢?也许你会想下面这么做:
exitFromRoutine:
;; return to _start
ret
这段代码能够正常工作吗?不能,还记得怎么在_start调用calculateStrLength的?当我们调用一个函数时需要注意什么?首先所有的函数参数从右到左压入栈中。返回地址也压入栈中,当执行完函数后,函数能够从栈中获取返回地址,从而执行下一条指令。但是我们在calculateStrLength函数中更改了栈,从而导致函数返回无法找到返回地址,那么我们怎么办呢?也许你还记得没有解释的下面这个指令:
mov rdi, $ + 15
我们需要先知道一些nasm的知识:
- $ - 返回定义?返回定义的内存地址
- $$ - 返回当前段的其实地址
知道$返回的当前定义的地址后,那么指令为什么是mov rdi, $ + 15 ,让我们使用objdump反汇编代码看看:
objdump -D reverse
reverse: file format elf64-x86-64
Disassembly of section .text:
00000000004000b0 <_start>:
4000b0: 48 be 41 01 60 00 00 movabs $0x600141,%rsi
4000b7: 00 00 00
4000ba: 48 31 c9 xor %rcx,%rcx
4000bd: fc cld
4000be: 48 bf cd 00 40 00 00 movabs $0x4000cd,%rdi
4000c5: 00 00 00
4000c8: e8 08 00 00 00 callq 4000d5
4000cd: 48 31 c0 xor %rax,%rax
4000d0: 48 31 ff xor %rdi,%rdi
4000d3: eb 0e jmp 4000e3
可以看到第十二行 (mov rdi, $ + 15)使用了10个字节,函数调用使用了5个字节,所以总的是15个字节。这就是为什么是增加15而不是其他的值,现在,让我们将这个值压入栈中作为函数返回的地址:
exitFromRoutine:
;; push return addres to stack again
push rdi
;; return to _start
ret
这样后,我们就返回到了_start中,然后将RAX置为0,然后跳转到reverseStr标签处。
reverseStr:
cmp rcx, 0
je printResult
pop rax
mov [OUTPUT + rdi], rax
dec rcx
inc rdi
jmp reverseStr
这段代码很简单,主要就是检查RCX是否为0,如果不为0就将栈中的数据弹出到OUTPUT缓冲区中,通过使用额外的RDI寄存器来记录当前缓冲区的偏移,当RCX为0后就跳转到printResult打印结果。
指令完reverseStr后,反转的字符串就保存到OUTPUT中,然后调用printResult打印一个换行符结束的字符串:
printResult:
mov rdx, rdi
mov rax, 1
mov rdi, 1
mov rsi, OUTPUT
syscall
jmp printNewLine
printNewLine:
mov rax, SYS_WRITE
mov rdi, STD_OUT
mov rsi, NEW_LINE
mov rdx, 1
syscall
jmp exit
然后退出程序:
exit:
mov rax, SYS_EXIT
mov rdi, EXIT_CODE
syscall
使用几个简单的Makefile编译程序:
all:
nasm -g -f elf64 -o reverse.o reverse.asm
ld -o reverse reverse.o
clean:
rm reverse reverse.o
然后运行这个程序:
字符串操作
有很多的字符串年操作指令:
- REP - 重复操作直到RCX为0
- MOVSB - 拷贝字符串(MOVSW,MOVSD等等)
- CMPSB - 字符串比较
- SCASB - 字符串扫描
- STOSB - 写字节到字符串