快速入门Linux下GDB和汇编开发工具

2016-11-15  本文已影响686人  QihuaZhou

文章也同时在个人博客 http://kimihe.com/更新

引言

本文亦是《读笔 汇编语言-基于Linux环境(第7章-跟踪指令:与机器指令亲密接触I)》。

本文将会以一个简单的.ASM程序,step by step地帮助大家快速入门GDB,并通过GDB调试,深入底层阐述高级语言(如C语言)中循环结构和指针的由来。

通过阅读本文,你将知道:

构建汇编程序

原料

第一个汇编程序

切到你喜欢的工作目录下,执行> kate以启动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了,在Terminal中键入:> gdb sandbox以启用gdb调试。

调试,我们一般都会需要设置断点,来看看各变量的情况。这里我们已经更加深入到底层,不在内存中操作了,直接来到了CPU内部的寄存器中。键入:> b 10即在DoMore: add byte [ebx], 32语句处加入断点。

然后,键入:> r然程序开始运行。程序会停在DoMore语句那里,看起来就像这样:

step1

接着,键入:i r查看个寄存器状态,就像这样:

step2

你可以看到高亮的绿色部分,rax中存有字符串长度8,rbx中存有字符串地址。

啥?为什么不是eax和ebx?嗯,很有价值的问题,eax和ebx是32位CPU架构下的寄存器,而如今64位已经普及,我们的寄存器也随之升级了。

然后按一下Enter键,或者输入return,可以看到下一页未显示完全的一些寄存器:

step3

注意到绿色高亮部分的eflags标志位,我们发现其中除了IF什么都没有,这表明我们上文提到的ZF标志还没有被激活。

接下来,键入:s,它代表单步执行一行语句,请先执行一次,然后再键入:i r看一下结果寄存器状态:

step4

可以看到rax寄存器内部的值从8减到7,表明执行了一次循环中的dec指令。接下来你可以继续单步执行7次,即键入7次s。每一次都查看一下寄存器的状态,你会发现rax不断递减,直到0。7次单步之后,再次键入i r进行查看:

step5

你会发现rax变成0了,此时Enter到下一页,我们发现:

step6

没错!eflags中出现了ZF标志,表明其被激活,这样jnz就不会再跳到DoMore,循环终于结束了。

最后请键入q,然后y退出GDB。我们的GDB快速入门到此告一段落。

留一个小问题

看到这儿,相信你已经大概理解了循环结构和指针的原理,对汇编工具以及GDB的使用也略知一二。那么我在这里提一个小问题:这段汇编代码到底是做什么的?请你积极思考哦~

答案我会在留言中说明。

总结

本篇文章通过Linux下的一个最简易的汇编开发流程,带领大家熟悉了开发工具的使用,并入门了GDB这一神器。同时通过阅读汇编代码,从底层理解了循环结构和指针的原理。希望对大家有所启迪,感谢阅读!

微信公众号

第一时间获取最新内容,欢迎关注微信公众号:「洛斯里克的大书库」。


微信公众号「洛斯里克的大书库」
上一篇下一篇

猜你喜欢

热点阅读