前面的章节介绍了汇编开发的硬件知识和相关的开发工具,是时候接触一些具体的例子来研究汇编语言了...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

    本篇翻译对应汇编教程英文原著的第104页到第112页 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为112 / 577)。

    前面的章节介绍了汇编开发的硬件知识和相关的开发工具,是时候接触一些具体的例子来研究汇编语言了。

The Parts of a Program (汇编程序的组成部分):

    汇编程序是由不同的段组成的,每个段都有各自的不同的作用,其中三个最常用的段如下:
  • data section (数据段)
  • bss section (bss未初始化数据段)
  • text section (代码段)
    text section(代码段)是所有汇编程序中都必需的段,该段里存放了程序里的可执行代码部分。数据段和bss(未初始化数据段)是可选的,不过在汇编程序中也被经常用到。data数据段主要包含已初始化的数据,这些数据被用作汇编程序中的变量,bss未初始化段主要用作程序中的缓冲区域,这些缓冲区域最开始被零填充。

    下面就对上面这些段在GNU汇编器中的使用进行说明。

Defining sections (段的定义):

    GNU汇编器使用.section的伪操作符来声明段,通过.section加一个参数来代表段的类型,下图显示了汇编程序中段的布局:


图1

    上图是汇编程序中段的常规布局,bss段应总是放在text代码段之前,但是data数据段可以移到text代码段之后,为了方便代码的可读和可维护性,最好将所有的数据定义都放在代码的开头部分。

Defining the starting point (定义程序的入口点):

    当汇编程序转成可执行程序时,链接器必须知道你的程序的入口点在哪,对于很简单的程序,找到入口点并不难,但是如果你的程序中有好几个函数,那么就需要手动来指定执行入口。

    要解决这个问题,GNU汇编器里提供了一个默认的标签,即_start标签来指示程序的执行入口,如果链接器找不到该标签,就会产生如下的警告信息:

$ ld -o badtest badtest.o
ld: warning: cannot find entry symbol _start; defaulting to 08048074
$

    从上面的输出可以看出,链接器没找到_start时,就会自动尝试指定一个执行入口,对于简单的程序可能不会出错,但是如果程序很复杂的情况下,链接器的自动查找可能就会不准。

    小提示:当然你可以使用别的非_start的标签名来表示执行入口,不过需要给GNU的链接器提供一个-e的参数来指明新的入口标签名。

    另外,除了声明一个入口标签外,你还需要让这个入口点可以被程序中的其他模块访问,此时就需要借助.globl伪操作符了。

    .globl伪操作符用于声明那些可以被外部程序模块访问的标签,例如你可以在某个汇编程序中编写一些辅助库函数,然后用.globl伪操作符来声明这些函数的标签,那么其他汇编程序或C程序中就可以通过这些标签来访问到辅助库函数了。

    有了这些信息,你就可以创建一个基本的汇编程序的模板了,该模板应该是下面这个样子:

.section.data

    < initialized data here 初始化数据部分>

.section .bss

    < uninitialized data here 未初始化数据部分>

.section .text
.globl _start
_start:

    <instruction code goes here 具体的指令代码部分>

    有了这个模板,你就可以开始编写具体的汇编程序了,下面就介绍汇编程序的简单例子。

Creating a Simple Program (创建一个简单的程序):

    将要创建的程序中使用了一个CPUID的指令代码,该指令代码用于收集处理器的供应商和型号等信息,下面就具体的介绍下该指令。

The CPUID instruction (CPUID指令):

    CPUID指令使用一个寄存器的值作为输入,即根据EAX里的值判断需要产生什么信息,然后将信息输出到EBX , ECX 及 EDX寄存器中,存放在这些寄存器里的信息,有的是一些位标志,需要解释成合适的含义才行。下表是EAX的值和CPUID输出信息的对应关系:

EAX Value 
(EAX输入值 , 功能号)
CPUID Output 
(CPUID指令对应的输出信息)
0 Vendor ID string, and the maximum 
CPUID option value supported 
处理器的供应商ID字符串信息,
以及CPUID所支持的最大的基本功能号
1 Processor type, family, model, and 
stepping information 
处理器类型,家族型号以及CPU的功能信息
2 Processor cache configuration 
处理器的缓存配置信息
3 Processor serial number 
处理器的序列号信息,在奔腾3中被引入,
但是由于隐私问题,在后面的型号中都没实现,
AMD的CPU则在任何型号中都没实现过该功能
4 Cache configuration (number of threads, 
number of cores, and physical properties) 
缓存配置(线程数,核心数及相关的物理属性)
5 Monitor information 
显示器相关信息
80000000h Extended vendor ID string and 
supported levels 
处理器的扩展的供应商ID字符串信息以及
所支持的扩展的功能
80000001h Extended processor type, family, model, 
and stepping information 
扩展的处理器类型,家族型号,及功能位信息
80000002h - 80000004h Extended processor name string 
处理器的品牌名称字符串信息

    后面的示例程序将利用功能号0来获取处理器的供应商ID字符串信息,通过设置EAX为0,然后执行CPUID指令,处理器就会将供应商ID字符串信息存放到EBX , EDX 及 ECX寄存器中,这三个寄存器里各自存放一部分字符串信息:
  • EBX里将包含字符串的前4字节
  • EDX里将包含字符串的中间4个字节
  • ECX里将包含字符串的最后4个字节
    寄存器里的字符串信息是以小字节序的形式存放的,也就是说字符串的开头部分放在寄存器的低位部分,下图显示了字符串的各个字节在寄存器里的布局:


图2

    示例程序将提取出寄存器里的这些值,然后以可读的形式显示出来,接下来就进入示例程序的源代码部分。

    小提示:需要注意的是,并不是所有的处理器都支持CPUID指令,对于一些老古董的处理器,最好在使用CPUID指令前先测试一下,测试的方法前面提到过,就是如果eflags里的ID标志位可以被设置或清零的话,那么就说明处理器支持CPUID指令,下面的示例程序为了简化起见,并没有做这方面的测试。

The sample program (示例程序):

    下面的代码将使用CPUID指令来获取处理器的厂商字符串信息,你可以将代码复制到某个文本文件中,并命名为cpuid.s :

#cpuid.s Sample program to extract the processor Vendor ID
.section .data
output:
    .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"
.section .text
.globl _start
_start:
    movl $0, %eax
    cpuid
    movl $output, %edi
    movl %ebx, 28(%edi)
    movl %edx, 32(%edi)
    movl %ecx, 36(%edi)
    movl $4, %eax
    movl $1, %ebx
    movl $output, %ecx
    movl $42, %edx
    int $0x80
    movl $1, %eax
    movl $0, %ebx
    int $0x80

    这个程序中涉及到了好几个不同的汇编指令,这些指令将在后面的章节中依次进行详细的解释,这里只要了解程序的结构,和指令的大概意思就可以了。

    首先,在data数据段部分,声明了一个字符串类型的变量:

output:
    .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n"

    .ascii伪操作符用于声明使用ASCII字符的文本字符串,该字符串会被预置到内存中,output标签指向该字符串的起始位置,这样在主体程序中就可以使用output来引用这段字符串信息,字符串中的"xxx..."部分将会在程序中被具体的处理器信息覆盖掉。

    data数据段后面紧跟着就是text代码段,该代码段部分声明了程序的执行入口:

.section .text
.globl _start
_start:

    _start执行入口后面就是具体的汇编代码了,代码中先将EAX寄存器的值设置为0,然后运行CPUID指令:

movl $0, %eax
cpuid

    上面的代码表示,使用CPUID的0号基本功能来获取处理器的供应商ID字符串信息,CPUID运行完后,EBX , EDX , ECX三个寄存器中就包含了输出的结果:

movl $output, %edi
movl %ebx, 28(%edi)
movl %edx, 32(%edi)
movl %ecx, 36(%edi)

    上面的代码,先将output标签指向的内存位置加载到EDI寄存器中,这样EDI寄存器就指向了data数据段部分"The processor Vendor ...."字符串的起始位置,28(%edi)表示访问或设置edi加28对应的内存位置,该内存位置刚好指向了"xxx..."部分的第一个字符的位置,接着将EBX的值(里面存放了CPUID输出字符串的最低4个字节)传递到EDI加28的位置处,覆盖掉从左侧开始的4个"xxxx"字符,后面就依葫芦画瓢,将EDX(CPUID输出字符串的中间4个字节)传递到EDI加32的位置处,覆盖掉中间的"...xxxx..."4个'x'字符,最后将ECX里的值覆盖掉最后4个"xxxx"字符。上面4条指令执行完后,"xxx..."部分就完全变为处理器的供应商ID字符串信息了。

    output指向的字符串信息准备好后,接下来就是将这段信息显示出来:

movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80

    上面这段代码使用了linux内核中的系统调用(即int $0x80)来访问显示终端,int指令加上0x80会产生一个软件中断,linux内核接收并处理该中断,并根据EAX寄存器里的值来判断具体要执行哪个功能,如果没有软件中断和linux内核提供的这些功能,那么你就必须自己编写代码,将字符通过合适的I/O地址来输出显示。所以,使用linux系统调用可以为你的汇编程序开发节省很多的时间。

    提示:本章只需对linux系统调用有个大概的认识即可,完整的介绍将放在后面的章节中。

    上面的代码将4加载到EAX寄存器中,表示将使用4号系统调用功能,即将字节数据写入到某个文件中,该写文件系统调用相关的参数如下:
  • EAX中包含系统调用号
  • EBX中包含要写入的文件描述符
  • ECX中包含要写入的字符串的起始位置
  • EDX中包含要写入的字符串的长度
    在UNIX和类UNIX系统中,所有的东东都是以文件的形式来进行操作的,当前会话的显示终端即标准输出设备(STDOUT),对应的文件描述符是1 ,通过向该文件描述符写入数据,就可以将信息显示到屏幕上。

    代码中将output的内存位置加载到ECX中,表示要将output对应内存位置处的字符串给显示出来,将42加载到EDX中,表示要显示的字符串里有42个字符,当EAX , EBX , ECX , EDX 这4个参数寄存器里的值都准备好后,就可以调用int $0x80软件中断通过4号系统调用功能将output处的"The processor Vendor ID is ...."信息给显示输出到1号文件描述符代表的屏幕终端设备上。

    在处理器的供应商ID字符串信息显示出来后,就可以退出程序了:

movl $1, %eax
movl $0, %ebx
int $0x80

    退出程序也是通过linux系统调用,使用1号功能来退出程序,将0加载到EBX中,表示程序退出时返回给系统的退出码为0,你可以通过不同的退出码来表示不同的退出状态,比如用-1的退出码来表示程序发生了错误而退出,用0来表示成功执行代码后的正常退出,程序退出后会返回到shell命令提示符下。该系统调用功能类似于C语言中的exit函数。

Building the executable (将汇编代码转为可执行程序):

    我们将上面的代码保存为cpuid.s后,就可以通过下面的命令来生成可执行程序:

$ as -o cpuid.o cpuid.s
$ ld -o cpuid cpuid.o
$

    先通过as这个GNU汇编器将源代码转为cpuid.o的目标代码文件,接着使用ld(GNU链接器)将目标代码文件链接成可执行文件cpuid ,如果你的源代码中有错误,比如写错了某个指令助记符,那么汇编器就会将错误信息和对应的行号给显示出来:

$ as -o cpuid.o cpuid.s
cpuid.s: Assembler messages:
cpuid.s:15: Error: no such instruction: `mavl %edx,32(%edi)’

$

    上面就显示出了cpuid.s在第15行movl写成了mavl的错误。

Running the executable (运行生成的可执行文件):

    下面是在英文原著作者的MEPIS系统中的运行情况,该系统运行在奔腾4处理器下:

$ ./cpuid
The processor Vendor ID is ‘GenuineIntel’
$

    然后是在原著作者的Mandrake Linux 6.0系统中的运行情况,该系统运行在Cyrix 6x86MX处理器下:

$ ./cpuid
The processor Vendor ID is ‘CyrixInstead’
$

    程序的运行结果和预料的一样,Linux系统的好处就在于即使是很老的古董机,它都可以在上面跑起来,呵呵。

Assembling using a compiler (使用GNU的编译器来进行汇编):

    由于gcc会使用GNU汇编器来编译C代码,所以你也可以直接用gcc通过一步来完成汇编程序的汇编和链接工作。当然这并非常规做法,不过在某些情况下还是很有用的。

    不过使用gcc来汇编会有个小问题,那就是前面提到过GNU链接器会根据_start标签来判断程序的执行入口,但是gcc却通过main标签来判断执行入口(这是因为C和C++程序中经常用main函数来作为执行入口),所以你必须将汇编代码中的_start标签,包括.globl声明的部分都改为main ,如下所示:

.section .text
.globl main
main:

    修改保存好后,就可以通过下面的命令来汇编和测试:

$ gcc -o cpuid cpuid.s
$ ./cpuid

The processor Vendor ID is ‘GenuineIntel’
$

    可以看出来,是一样的效果,而且gcc只需要一行命令就得到了可执行程序。

    限于篇幅,先写到这,下节介绍汇编代码调试等内容。

    OK,到这里,休息,休息一下 o(∩_∩)o~~
上下篇

下一篇: 汇编开发示例 (二) 示例介绍结束篇

上一篇: 汇编开发相关工具 (三) 工具介绍结束篇,kdbg,gprof,mepis

相关文章

基本数学运算 (三) 除法和移位指令

使用IA-32平台提供的高级功能 (一)

高级数学运算 (二) 基础浮点运算

汇编函数的定义和使用 (一)

优化汇编指令 (二)

IA-32平台(二)