文章也同时在个人博客 http://kimihe.com/更新
引言
本文亦是《读笔 汇编语言-基于Linux环境(第7章-跟踪指令:与机器指令亲密接触I)》。
本文将会以一个简单的.ASM程序,step by step地帮助大家快速入门GDB,并通过GDB调试,深入底层阐述高级语言(如C语言)中循环结构和指针的由来。
通过阅读本文,你将知道:
- 如何快速在Linux下进行汇编开发。
- 如何快速入门GDB。
- 高级语言循环结构的原理。
- 指针到底是什么。
构建汇编程序
原料
- Linux环境,笔者是Ubuntu 12.04 LTS。
- 安装NASM: 新立得软件包管理器。
- 安装Kate编辑器和KWrite编辑器: 新立得软件包管理器。
- 安装konsole:
> sudo apt-get install konsole
第一个汇编程序
切到你喜欢的工作目录下,执行> kate
以启动Kate编辑器,启动后界面类似于这样:
左侧是导航栏,右侧是代码编辑区,下方是终端控制区(若要启用此特性请务必先安装konsle)。
新建一个文件,命名为sandbox.asm,在其中输入如下内容:
section .data
Snippet db "KANGAROO"
section .text
global _start
_start:
nop
; Put your experiments between the two nops...
mov ebx, Snippet
mov eax, 8
DoMore: add byte [ebx], 32
inc ebx
dec eax
jnz DoMore
; Put your experiments between the two nops...
nop
这段代码是我们第一个汇编小例子,用于阐明循环结构的原理,请确保文章例子和你的完全一致。
循环结构的原理
如果是首次接触汇编,你可能会一头雾水,在这里你不必在意汇编的语法,只需要理解我对代码的说明即可。
此处请先注意语句Snippet db "KANGAROO"
,其中Snippet代表一个字符串,内容为KANGAROO。然后注意语句mov ebx, Snippet
,这一步相当于获取字符串的首地址。紧接着的mov eax, 8
用于获知字符串的长度。这两步很平常,高级语言的字符串处理也需要获知字符串地址以及相应的长度。
然后请关注如下四条语句:
DoMore: add byte [ebx], 32
inc ebx
dec eax
jnz DoMore
此处的jnz DoMore
语句便是循环结构的核心。其含义是:jnz(Jump if Not Z-Flag)进行判断,如果零标志位ZF不为0,就跳转到DoMore语句处。
于是你可以想到,只要这个ZF标志位不是0,程序就会不停地循环跳转(loop),循环结构由此而来。
你可能会想问:什么时候ZF会变成0?这个问题很好,试想一下高级语言的while(n)循环,我们必然需要一个操作步骤来改变n的值,使其在某一时刻变成0,从而跳出while。
此处,眼尖的读者可能发现了dec指令,还记得一开始的获取字符串长度为8吗?我们把8存在了eax寄存器中(如果你不清楚寄存器是什么,也没有关系,把它想象成一个可以存放数值容器即可)。通过dec eax
指令,我们会不断地对eax中的8进行递减,类似于int eax = 8; eax--;
总有一天,eax中的值会从8减到0,此时我们的x86 Intel CPU就会执行一项既定的操作,把ZF标志设为1,以代表此标志位处于激活状态。于是,jnz在判断的时候就发现ZF已经被激活为1了,不需要再跳转,循环结果宣告结束。
此外,不知道你有没有对于jnz跳转指令产生一些联想:它是不是很像函数指针?(jnz到一个地方,那个地方叫做DoMore,然后执行一段过程。)当然关于函数指针详细的说明,本文篇幅可就不够了,笔者会考虑以后单独写一篇文章详细说明,敬请期待~
什么是指针
有读者可能会问,还有两行代码没有解释呢。不要着急,这两行代码蕴含着指针的奥秘。听起来可能有点令人惊奇,但实际情况确实如此。让我们来看一下这两行代码:
DoMore: add byte [ebx], 32
inc ebx
注意add byte [ebx], 32
这句话,它的专业术语叫做寄存器间接寻址。它是如此神奇,毫不夸张地说,如果没有它,我们日常所见的绝大部分程序将难以构建。
这句话解释一下就是这样:有一个内存单元,它有一个byte大小的空间,里面存有一个数值n(具体是多少,现在不用关心)。把数值32 Add到这个n上,就是相当于n+=32
。然后关键点来了,为了加上32,我们需要知道这个内存区域在哪儿。在哪儿呢?在ebx里存着呢!
内存就像一个个信箱,每个信箱都有自己的编号,当我们寻找自家的信箱时,会根据信箱的编号去寻找它。这里ebx就存着我们要的内存区域的编号,这个编号叫做地址,根据这个地址,我们找到了那个内存单元的具体位置,然后知道了其中存了一个数n,最后把32给加到了n上。
这里,你应该可以看到,我们并不是直接去访问那个数值n的,而是先去找存放它的内存单元。这里面存在一层间接。正是有了这层间接,我们才能在高级语言中构筑起各种华丽的调用操作。
于是指针的原理也显而易见了,对于
char arr[4] = "abcd";
char *p = arr;
p+=3;
printf("*p: %c\\n", *p);
我们char *p = arr;
操作定位的arr数组的首地址。arr信箱有四个格子,我们定位到第一个,然后p+=3;
并不是直接给信箱什么的加3,这明显不符合逻辑,而是操作信箱的编号(地址)。加3意味着往后数三个,定位到第四个格子,最后打印里面的东西,就是字符d。
内存中的数据有两种,分别是数据和地址,数据就是普通的变量,地址就是指针。希望你不要混淆。
使用GDB
下面进入最后一个知识点,快速入门GDB。在此之前,我们需要把编写的.ASM程序编译链接运行起来。你可能听说过Linux下的make工具,说白了就是个配置文件,告诉NASM,gcc等编译器怎么有效地编译我们的源码,避免重复劳动。make配合makefile文件工作,如果你不知道这到底是什么,也完全没有关系,毕竟这不是本文的重点,只是顺带提一下。
你可以在Kate编辑器中再新建一个文本,名为makefile,请确保它和我们的sandbox.asm在同一个目录下。向其中输入如下内容:
sandbox: sandbox.o
ld -o sandbox sandbox.o
sandbox.o: sandbox.asm
nasm -f elf64 -g -F stabs sandbox.asm -l sandbox.lst
你可以完全不必理会这四句话到底代表了什么,只需要明白它们会让NASM正确地生成我们的.ASM程序。
有了这个makefile,接下来可以在Kate编辑器下方的terminal中输入> make -k
,或者你自己启动shell,切到你的工作目录,执行上述命令。如果正确编译完成,那么看起来就像这样子:
接下来,我们要使用GDB了,在Terminal中键入:> gdb sandbox
以启用gdb调试。
调试,我们一般都会需要设置断点,来看看各变量的情况。这里我们已经更加深入到底层,不在内存中操作了,直接来到了CPU内部的寄存器中。键入:> b 10
即在DoMore: add byte [ebx], 32
语句处加入断点。
然后,键入:> r
然程序开始运行。程序会停在DoMore语句那里,看起来就像这样:
接着,键入:i r
查看个寄存器状态,就像这样:
你可以看到高亮的绿色部分,rax中存有字符串长度8,rbx中存有字符串地址。
啥?为什么不是eax和ebx?嗯,很有价值的问题,eax和ebx是32位CPU架构下的寄存器,而如今64位已经普及,我们的寄存器也随之升级了。
然后按一下Enter键,或者输入return,可以看到下一页未显示完全的一些寄存器:
注意到绿色高亮部分的eflags标志位,我们发现其中除了IF什么都没有,这表明我们上文提到的ZF标志还没有被激活。
接下来,键入:s
,它代表单步执行一行语句,请先执行一次,然后再键入:i r
看一下结果寄存器状态:
可以看到rax寄存器内部的值从8减到7,表明执行了一次循环中的dec指令。接下来你可以继续单步执行7次,即键入7次s
。每一次都查看一下寄存器的状态,你会发现rax不断递减,直到0。7次单步之后,再次键入i r
进行查看:
你会发现rax变成0了,此时Enter到下一页,我们发现:
没错!eflags中出现了ZF标志,表明其被激活,这样jnz就不会再跳到DoMore,循环终于结束了。
最后请键入q
,然后y
退出GDB。我们的GDB快速入门到此告一段落。
留一个小问题
看到这儿,相信你已经大概理解了循环结构和指针的原理,对汇编工具以及GDB的使用也略知一二。那么我在这里提一个小问题:这段汇编代码到底是做什么的?请你积极思考哦~
答案我会在留言中说明。
总结
本篇文章通过Linux下的一个最简易的汇编开发流程,带领大家熟悉了开发工具的使用,并入门了GDB这一神器。同时通过阅读汇编代码,从底层理解了循环结构和指针的原理。希望对大家有所启迪,感谢阅读!