作者:(美)布鲁姆 出版社:机械工业出版社
###第四章###汇编语言程序范例
**4.1**程序的组成
汇编语言由定义好的段构成,每个段都有不同的目的。
常用段:
· 数据段
· bss段
· 文本段
所有汇编语言程序中必须有文本段。文本段是在可执行程序内声明指令码的地方,也就是比如mov,push,pop这些指令码助记符(简称指令码)声明的地方。数据和bss段可选的,但是在程序中经常使用到。数据段声明带有初值的数据元素,这些数据元素用作程序中的变量。bss段声明使用零(或NULL)初始化的数据元素。这些数据元素用作汇编语言程序中的缓冲区。
4.1.1 声明段
GNU汇编器使用 .section命令声明段。只有一个参数:声明段的类型。
———————
| .section .data |
| .section .bss |
| .section .text |
________________
一个汇编语言程序结构布局大致如上所示。
一般情况下,bss段总是安排在文本段之前,但是数据段可以移动到文本段之后,虽然这不是什么标准。除了完成程序逻辑和实现的功能,汇编语言程序还应该易读。
4.1.2 定义起始点
当汇编语言被转化为可执行文件时,连接器必须知道指令码的起始点什么。但是对于只有单一指令路径的简单程序,找到起始点相对容易。但是对于使用分散在源代码各个位置的若干函数的更加复杂的程序,发现程序从哪里开始不是件容易的事情。
对此,GNU汇编器定义了起始标签:_start。_start标签用于表明程序应该从哪条指令开始运行。另外,如果连接器找不到这个标签,将会发生错误。
<除了在应用程序中声明起始标签之外,还需要为外部应用程序提供入口点,这是用.globl命令完成>
.globl命令声明外部程序可以访问的程序标签。如果编写被外部汇编程序或者C语言程序用的一组工具,就应该使用.globl命令声明每个函数段标签。
来一个模板:
.section .data
< initialized data here >
.section .bss
< uninitialized data here >
.section .text
.globl _start
_start:
< instruction code goes here >
接下来展示如何从汇编语言程序源代码构建应用程序。
**4.2**创建简单的程序
我们创建一个重点在单一指令码的简单应用程序。CPUID指令码用于运行程序处理器的信息。可以获得厂商和型号信息,并且显示出来。
4.2.1 CPUID指令
CPUID是一条汇编语言指令,不容易从高级语言执行它。它请求CPU的特定信息,并且把返回信息放回到特定寄存器中的低级指令。
cpuid使用单一寄存器作为输入。eax寄存器用于决定cpuid指令生成什么信息。根据eax中的值,cpuid指令在ebx,ecx,edx寄存器中生成关于处理器的不同信息。信息以一些列位值和标志的形式返回,必须解释出他们的正确含义。
==============================================
eax值 cpuid输出
———————————————————————————————-
0 厂商ID字符串和支持的最大cpuid选项值
1 处理器类型,系列,型号和分步信息
2 缓存配置
3 处理器序列号
4 缓存配置
5 监视信息
80000000h 扩展的厂商id字符串和支持的级别
80000001h 扩展的处理器类型,系列,型号和分步信息
80000002h~800000004h 扩展的处理器名称字符串
=================================================
现在我们使用0选项,从处理器获取简单的厂商ID字符串。当0值被放入eax寄存器并且执行CPUID指令时,处理器把厂商id返回到ebx,edx,ecx寄存器中
·ebx包含字符串最低4字节
·edx包含字符串中间4字节
·ecx包含字符串最高4字节
按照小尾数格式存放在寄存器中。小尾数存放是Intel处理器存放数据的方式:简单地说就是低字节首先存放到寄存器中,然后再一次存放中间字节和高字节。
4.2.2范例程序
了解了cpuid指令如何工作后,现在编写程序实现这个过程。
.section .data
output:
.ascii “The CPU is ‘xxxxxx’\n”
.section .text
.globl _start
_start:
movl $0, %eax #使eax加载0值,
cpuid #然后运行cpuid指令;其中的0值指定的是cpuid指令的输出选项(这里输出的是CPU厂商id字符串)
movl $output, %edx #创建一个指针。处理output时会用到真个指针:也就是将标签output的地址储存到edx寄存器中。
movl %ebx, 28(%edx) #括号外的数字表示相对于output标签的偏移量。
movl %edx, 32(%edx) #这些数字和edx的值相加就确定了ebx,edx,ecx中的值被写入的地址。
movl %ecx, 36(%edx)
movl $4, %eax #Linux内核系统调用号。这句话是说调用write()这个系统调用
movl $1, %ebx #这时描述符1,表示stdout
movl $output, %ecx #要输出的字符串起始地址
movl $42, %edx #字符串长度
int $0x80 #软中断
movl $1 %eax #系统调用号1,这句话相当于调用exit()
movl $0, %ebx #向shell返回0,表示成功推出程序
int $0x80 #软中断
然后就是涉及到编译和链接的问题,不在赘述
**4.3** 调试程序
1.单步运行程序
使用编译参数-gstabs,可以使得可执行程序包含必要的调试信息。
为了单步运行程序,必须设置断点。简单的说,断点设置在程序中调试器希望程序停止运行的地方。
可以在下列情况下停止程序运行:
· 到达某个标签
· 到达源代码中某个行号
· 数据值到达某个特定的值
· 函数执行了特定的次数
现在我们将断点设置到程序代码的开头。需要指出的是,汇编语言中断点的设定要指定对于最近标签的相对位置。
格式:
break *label + offset
label是源代码中的标签,offset是执行应该停止的地方相对于这个标签的行数。
为了在第一条指令处设置断点,然后启动程序,输入入戏命令:
break *_start
现在程序在_start标签处停了下来,并且指出下一条指令是movl $0, %eax
接着可以使用 next或者 step 命令一步一步执行程序。当我们感兴趣的段落执行之后,可以使用 cont 指令继续执行直到程序结束。
2.查看数据
既然知道了程序怎么让程序在特定位置停止下来,现在来看看怎么检查停止时的数据元素。
gdb命令有以下:
==================================================
数据命令 描述
——————————————————————————————————
info register 查看所有寄存器的值
print 显示特定寄存器或者程序变量值
x 显示特定内存位置的内容
==================================================
ok!现在我们来详细跟踪这个程序的运行,这个跟踪过程我们要查看各个寄存器、各个程序变量值、各个内存地址的变化。
->设置断点:break *_start
运行起来:run 显示了即将运行:movl $0, %eax
查看各个寄存器:info register
显示通用寄存器(eax,ecx, ebx, edx)都为0
重点关注eip,他指向了下一条指令的地址。
单步运行:step,知道下一条指令是movl $output, %edx
下面说个例子,比如现在我们感兴趣,output标签的内存地址到底是多少?有没有被存放到edx寄存器?
查看初始edx寄存器值:print/x $edx 显示:0x49656e69
查看output标签内存地址(注意只是内存地址):print/x &output 显示:0xB0490ac
继续运行:next
查看寄存器:info register 发现edx寄存器确实被改写成了output标签的内存地址。
通过以上知道了查看寄存器、程序变量的内存位置、查看指定寄存器值的方法。
但是我们只是知道了如何查看变量的内存位置,却还不知道内存位置的内容。也就是内存位置到底存放的是什么还不知道。
使用命令:x
格式:x/nyz
其中n是要显示的字段数。
y是输出格式,可以是:
· c显示字符
· d用于显示十进制
· x用于显示十六机制
z是要显示的字段的长度:
· b用于字节
· h用于16位字
· w用于32位字
例如,要显示output标签的内存位置的值:
x/42cb
表示以字符模式显示变量output变量的前42个字节,&表示output是一个内存位置。
这个特性很有价值。
(未完待续)
=========接下来是第四章关于调用C语言库函数的问题===========