本文由zengl.com站长对
http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。
另外再附加一个英特尔英文手册的共享链接地址:
http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)
本篇翻译对应汇编教程英文原著的第122页到第131页 (注意这里的页数不是页脚的页数,而是pdf电子档顶部,分页输入框中的页数,也就是包含了目录,前言部分的总页数,pdf电子档总页数是577,当前已翻译完的页数为131 / 577)。
下面先介绍如何在汇编中定义数据,接着介绍如何在内存,寄存器之间传递数据。
Defining Data Elements (数据元素的定义):
下面分别介绍data数据段和bss未初始化数据段中,数据的定义方法。
The data section (数据段):
data数据段部分是程序中定义数据元素最常见的地方,前面提到过,通过
.data伪操作符就可以定义数据段,汇编程序中的指令可以读写数据段中的任何数据元素。
小提示:还有一种特殊的只读数据段,可以通过.rodata进行声明,该段里的任何数据元素都是只读的,所以是ro为前缀(即read-only的缩写)。
data数据段中的数据元素可以分为两部分,一部分是标签名,标签名就像C程序中的变量名,在汇编程序中可以通过标签名来引用标签所在的内存位置。
另一个就是用于定义数据元素的数据类型部分,有很多种数据类型可供选择,不同的数据类型表示该数据元素需要占用多少内存空间,下表显示了不同数据类型对应的伪操作符:
Directive |
Data Type |
.ascii |
Text string
文本字符串 |
.asciz |
Null-terminated text string
以null字符结尾的文本字符串,C的标准库函数经常需要处理这种类型的字符串,因为它需要通过null(ASCII值为0的字符)来判断字符串的有效长度 |
.byte |
Byte value
字节数据 |
.double |
Double-precision floating-point number
双精度浮点数 |
.float |
Single-precision floating-point number
单精度浮点数 |
.int |
32-bit integer number
32位整数 |
.long |
32-bit integer number (same as .int)
和.int一样,代表32位的整数 |
.octa |
16-byte integer number
16字节的整数(1个字节是8位) |
.quad |
8-byte integer number
8字节的整数 |
.short |
16-bit integer number
16位整数 |
.single |
Single-precision floating-point number (same as .float)
和.float一样,代表单精度浮点数 |
|
在数据类型伪操作符后面,必须定义该数据元素的默认初始值,这样在程序执行时,该保留内存位置一开始就会被设置为这个值,直到被程序修改为止。
下面是在data数据段中定义一个数据元素的例子:
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n" |
该例子在前面的章节中已经讨论过了,
.ascii用于声明该内存里存放的是文本字符串,字符串中每个字符占用1个字节,所以系统会为程序在数据段中保留一块42个字节的内存,然后将上面这段字符串设置到该内存中,output标签则指向了这段内存的起始位置即字符串信息的第一个字符的位置,程序里通过引用output标签名就可以访问这段内存中的字符串信息。
下面的例子定义了一个浮点数:
上面这段代码的含义是:将3.14159的浮点操作数设置到pi标签所引用的内存位置处。
小提示:后面的章节中会详细的介绍浮点数在内存中的存储方式。
当然你还可以在数据类型伪操作符后面定义多个值,这些值会按照伪操作符后面出现的先后顺序排列到内存中,从而构成一个类似C语言中的数组,如下所示:
sizes:
.long 100,150,200,250,300 |
sizes指向的内存中存放了5个长整数:100 , 150 , 200 , 250和300,sizes指向第一个元素100 ,每个数都是4字节的长整数,所以可以使用sizes+8来访问第三个元素200(不过需要注意访问该元素的时候,需要使用4字节的方式进行读取,否则就会读到错误的值)。
根据需要,你可以在data数据段中定义很多数据元素,还可以在每个数据元素中定义很多同类型的值,只是有一点需要注意:标签名必须放在数据类型伪操作符的前面,下面的例子就定义了多个数据元素:
.section .data
msg:
.ascii "This is a test message"
factors:
.double 37.45, 45.33, 12.30
height:
.int 54
length:
.int 62, 35, 47 |
每个数据元素都是按照它们在data数据段中出现的顺序排列在内存中的,并且这些数据都是仅挨着的,中间没有多余的字节空隙,数据元素中的多个值也是按先后顺序进行排列的,下图显示了这些数据在内存中的布局:
图1
从上图可以看出来,第一个数据元素位于数据段内存的最低位置,height后面紧跟着就是length数据元素,length中的三个值也是按出现的顺序从内存的低地址排列到高地址的。
小提示:在程序中使用定义过的数据元素时,需要特别注意指令中操作数据的大小,例如,假设你定义了两个16位的整数,这两个整数挨在一起,但是指令访问第一个元素时,如果使用32位大小来引用第一个元素的话,汇编器就会把两个值当作一个值读出来,这样原先是想访问16位的整数,结果却访问到32位的值出来,结果肯定就不对了,还有,如果你使用32位大小访问第二个16位的整数,而该整数后面没定义别的数据时,那么32位的方式进行访问就不知道会得到什么结果了。
Defining static symbols (定义静态常量):
在data数据段中除了可以定义那些可变的数据元素外,你还可以定义一些静态的常量数据,通过
.equ伪操作符就可以定义程序中需要使用的常量:
.equ factor, 3
.equ LINUX_SYS_CALL, 0x80 |
一旦设置了常量,那么这些常量的值就不能在程序中被修改,
.equ伪操作符可以出现在data数据段的任何地方,不过为了方便维护,最好将所有的常量数据都统一定义在其他数据元素的前面或后面。
要在程序中引用这些静态数据元素,你必须在标签名前面添加美元符,例如下面的指令:
movl $LINUX_SYS_CALL, %eax |
上面的指令将前面.equ定义的LINUX_SYS_CALL符号对应的常量值给传递到EAX寄存器中。
The bss section (bss未初始化数据段):
bss段和data段不同,你不需要为其中的数据元素指明数据类型,只需指定缓冲区域所需要的大小即可,至于你用该缓冲区域做什么,那就是根据你的需求来定了。
GNU汇编器中有两种声明bss段中缓冲数据的伪操作符:
Directive
伪操作符 |
Description
描述 |
.comm |
Declares a common memory area for data that
is not initialized
声明一个通用的未初始化内存区域 |
.lcomm |
Declares a local common memory area for data that
is not initialized
声明一个局部通用的未初始化内存区域 |
|
.comm和.lcomm的区别在于,.comm声明过的缓冲数据在全局范围可见,可以在其他模块中引用,当ld链接器在多个模块中发现相同名称的.comm数据时,会对他们进行合并处理,所以.comm有点像C语言中的全局变量,而.lcomm只是在当前汇编代码中可见,在其他外部模块中无法引用,有点像C语言中的局部变量。
这两种伪操作符的使用格式如下:
.comm symbol, length
或者
.lcomm symbol, length |
上面的symbol就是缓冲区域的标签名,程序中可以用symbol来引用该缓冲区域对应的内存起始位置,length是该缓冲区域的字节大小,声明例子如下:
.section .bss
.lcomm buffer, 10000 |
这两行代码在bss段中定义了一个名为buffer的局部缓冲区域,该缓冲区域会被分配10000字节的大小,.lcomm声明过的局部符号如本例中的buffer不可以使用.globl进行声明。
使用bss段的好处是,在该段中定义过的数据大小不会包含进最终的可执行文件中,如上例,10000字节的buffer缓冲区域,最终的可执行文件中并不会真的包含这10000个字节,因为bss段的数据是未初始化的,所以没必要将这些未初始化的数据都包含进可执行文件,只需将缓冲区域的字节大小等信息写入可执行文件里,在可执行文件运行的时候再按照缓冲大小信息,为其分配所需的内存空间即可。而使用data数据段的话,由于该段里的数据都是初始化过的,有默认的初始值,所以必须包含进可执行文件中,如果将buffer这10000个字节的内存数据定义到data段的话,最终生成的可执行文件就会多出这10000个字节。
下面就举例来验证上面的说法,我们先写一个不包含任何数据元素的示例:
# sizetest1.s – A sample program to view the executable size
.section .text
.globl _start
_start:
movl $1, %eax
movl $0, %ebx
int $0x80 |
然后汇编和链接该程序,接着查看生成的文件大小:
$ as -o sizetest1.o sizetest1.s
$ ld -o sizetest1 sizetest1.o
$ ls -al sizetest1
-rwxr-xr-x 1 rich rich 724 Jul 16 13:54 sizetest1*
$ |
生成的可执行文件sizetest1大小为724字节,现在再创建一个测试程序,在该程序中添加一个bss未初始化段:
# sizetest2.s - A sample program to view the executable size
.section .bss
.lcomm buffer, 10000
.section .text
.globl _start
_start:
movl $1, %eax
movl $0, %ebx
int $0x80 |
汇编链接该程序,生成的文件大小如下:
$ as -o sizetest2.o sizetest2.s
$ ld -o sizetest2 sizetest2.o
$ ls -al sizetest2
-rwxr-xr-x 1 rich rich 747 Jul 16 13:57 sizetest2*
$ |
可以看到,虽然在bss段中声明了一个10000字节大小的buffer缓冲区域,但是生成的sizetest2文件只比前面的sizetest1文件增加了23个字节,并没有将10000个字节的大小添加进可执行文件中。
最后再写一个测试程序,该程序中使用data数据段来代替bss段:
# sizetest3.s - A sample program to view the executable size
.section .data
buffer:
.fill 10000
.section .text
.globl _start
_start:
movl $1, %eax
movl $0, %ebx
int $0x80 |
这次buffer被定义到data段中,通过
.fill伪操作符,汇编器会自动为你创建10000个字节的数据,并且这些数据全部用0填充。当然你也可以在buffer后面声明一个.byte的字节数据类型,然后手动列举10000个字节的数据,但是那样就太麻烦了。经过汇编和链接后,你可以看到生成的可执行文件的大小如下:
$ as -o sizetest3.o sizetest3.s
$ ld -o sizetest3 sizetest3.o
$ ls -al sizetest3
-rwxr-xr-x 1 rich rich 10747 Jul 16 14:00 sizetest3
$ |
这次输出的结果显示,sizetest3比前面的sizetest2文件多出了10000个字节,所以对于大的数据,如果不需要设置默认初始值的话,最好将其定义到bss段中,以减少生成的可执行文件的体积。
Moving Data Elements (数据移动操作):
在定义了数据元素后,你还必须知道如何操作它们。由于数据是位于内存中的,而很多处理器指令需要操作寄存器,所以我们首先就需要知道如何在内存和寄存器之间传递数据,MOV指令就可以完成这个操作,下面就详细介绍下MOV指令的用法。
The MOV instruction formats (MOV指令的格式):
MOV指令的基本格式如下:
MOV格式中的source和destination分别代表源操作数和目标操作数,源操作数和目标操作数可以是内存地址,存储在内存中的数据,定义在指令中的常量数据,或寄存器。
小提示:GNU汇编器使用的是AT&T的语法,所以源操作数和目标操作数的顺序是和Intel汇编语法相反的。
上面的基本格式中,mov后面有个'x' ,该符号是个通配符,代表操作数的大小,x可以是以下几种值:
-
l 代表32位的数据
-
w 代表16位的数据
-
b 代表8位的数据
因此如果要将32位的EAX寄存器里的值传递到32位的EBX寄存器中,你可以使用以下指令:
如果是在16位寄存器间移动数据,可以使用类似下面的指令:
对于8位的情况,可以使用如下指令:
当然,MOV指令中的源操作数和目标操作数的类型不可以随意搭配,下面是MOV指令中源和目标操作数可用的组合情况:
-
An immediate data element to a general-purpose register
将立即数传递到通用寄存器中
-
An immediate data element to a memory location
将立即数传递到一个内存位置
-
A general-purpose register to another general-purpose register
由一个通用寄存器传递到另一个通用寄存器
-
A general-purpose register to a segment register
将一个通用寄存器里的值传递到一个段寄存器
-
A segment register to a general-purpose register
将一个段寄存器里的值传递到一个通用寄存器
-
A general-purpose register to a control register
将一个通用寄存器里的值传递到一个控制寄存器
-
A control register to a general-purpose register
将一个控制寄存器里的值传递到一个通用寄存器
-
A general-purpose register to a debug register
将一个通用寄存器里的值传递到一个调试寄存器
-
A debug register to a general-purpose register
将一个调试寄存器里的值传递到一个通用寄存器
-
A memory location to a general-purpose register
将一个内存位置里的值传递到一个通用寄存器
-
A memory location to a segment register
将一个内存位置里的值传递到一个段寄存器
-
A general-purpose register to a memory location
将一个通用寄存器里的值传递到一个内存位置
-
A segment register to a memory location
将一个段寄存器里的值传递到一个内存位置
下面就详细的描述这些传值方式。
小提示:还有一个MOVS指令,它用于将某个内存位置处的字符串数据传递到另一个内存位置,不过该指令并不在此进行讨论,在后面的介绍字符串操作的章节中会进行介绍。
Moving immediate data to registers and memory (将立即数传递到寄存器和内存中):
立即数都是直接包含在指令代码中的,并且在运行时不能被修改。
下面是立即数传值的例子:
movl $0, %eax # moves the value 0 to the EAX register
movl $0x80, %ebx # moves the hexadecimal value 80 to the EBX register
movl $100, height # moves the value 100 to the height memory location
|
第一条指令将0传值给EAX寄存器,第二条指令将0x80传值给EBX寄存器,第三条指令将100传值给height标签所在的内存位置,所有立即数前面都必须添加美元符,立即数可以有不同的表达格式,如本例中的$0和$100都是十进制格式,$0x80则是十六进制格式,这些值在程序汇编链接成可执行程序后,就固化到指令的二进制码里了,所以这些值在运行时不能被改变。
Moving data between registers (在寄存器间传值):
在处理器的不同寄存器间进行传值是MOV指令的另一个基本功能,这是处理器传值中最快的方式了,所以通常应尽可能的将数据保存到寄存器中,以减少访问内存带来的时间上的开销。
处理器中有8个通用寄存器(EAX , EBX , ECX , EDX , EDI , ESI , EBP 和 ESP),这8个寄存器是存取数据中最常用的寄存器了。这几个寄存器除了可以相互之间进行传值外,还可以和其他类型的寄存器进行传值,其他类型的寄存器也只能和通用寄存器进行传值,如调试、控制、段寄存器等都只能将值传递给通用寄存器,或者反过来只能由通用寄存器将值传递给它们。
下面是寄存器间传值的例子:
movl %eax, %ecx # move 32-bits of data from the EAX to the ECX
movw %ax, %cx # move 16-bits of data from the AX to the CX
|
上面是相同尺寸的寄存器间进行传值,如果是不同尺寸寄存器间传值,就可能会出错,如下面这个例子:
这条指令会产生一个汇编器错误,源和目标操作数的大小不一致,al是8位,bx是16位,应该使用movw指令将%ax里的值传递到%bx中才对。
Moving data between memory and registers (在内存和寄存器间传值):
前面的寄存器间传值算是比较简单的功能,寄存器和内存间的传值就没那么容易了,下面分几种情形来进行说明:
Moving data values from memory to a register (将数据由内存传值到寄存器中):
在由内存传值给寄存器的过程中,你首先需要确定该如何表示数据所在的内存地址。最简单的方式就是使用标签名来表示内存地址:
该指令将value标签所在内存位置处的值传递给EAX寄存器,另一个需要注意的是传值的大小,上面代码中,mov助记符后面有个'l'字符,表示将value所在内存位置开始的32位即4个字节的数据给取出来传给EAX寄存器。如果你需要的数据小于4个字节,那么你就应该使用其他的MOV指令,如MOVB指令来传1个字节的数据,或者MOVW指令来传2个字节的数据。
下面再例举一个完整的例子来说明内存到寄存器间的传值:
# movtest1.s – An example of moving data from memory to a register
.section .data
value:
.int 1
.section .text
.globl _start
_start:
nop
movl value, %ecx
movl $1, %eax
movl $0, %ebx
int $0x80 |
现在使用-gstabs参数来汇编链接该程序,接着在调试器中运行程序:
$ as -gstabs -o movtest1.o movtest1.s
$ ld -o movtest1 movtest1.o
$ gdb -q movtest1
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file movtest1.s, line 10.
(gdb) run
Starting program: /home/rich/palp/chap05/movtest1
Breakpoint 1, _start () at movtest1.s:10
10 movl (value), %ecx
Current language: auto; currently asm
(gdb) print/x $ecx
$1 = 0x0
(gdb) next
11 movl $1, %eax
(gdb) print/x $ecx
$2 = 0x1
(gdb) |
可以看到,
movl value , %ecx 该指令执行前,ECX寄存器里的值是0x0 ,执行后,ECX寄存器里的值就变为0x1 ,和value内存中的值一样了,说明value中的值成功传递到ECX寄存器里了。
限于篇幅,先写到这,其他的传值情况下节再说。
OK,到这里,休息,休息一下 o(∩_∩)o~~