Linux汇编之程序范例

汇编 2015年02月28日 ,

ASM程序模板

在准备好汇编语言程序开发的工具后,就可以开始汇编语言程序设计。枫竹梦本文介绍汇编语言通用的格式,并构建一个汇编程序,同时尝试对其进行调试。

程序的组成

汇编语言(ASM)由定义好的段构成、每个段都有不同的目的。三个常用的段如下:

  • 数据段
  • bss段
  • 文本段

所有程序必须有文本段,其包含程序执行的指令码。数据和bss段是可选的。数据段声明带有初始值的数据元素,用作汇编语言的变量。bss段会初始化为零,常用作缓冲区。

定义段

GNU汇编器使用.section命令声明段。其只有一个参数用于说明段的类型。

.section .data
.section .bss
.section .text

bss段应该安排在文本段之前,但数据段可以放在文本段之后。

定义起始点

当汇编语言程序被转换为可执行程序时,连接器必须知道指令码的起始点是什么。GNU汇编器声明_start标签表明程序应该从这条指令开始执行。

注意:也可使用_start之外的其他标签作为起始点。使用连接器的-e参数定义新的起始点名称。

除了在应用程序中声明起始标签之外,还需要为外部应用程序提供入口点。使用.globl命令完成。.globl命令声明外部可以访问的程序标签。如果编写外部汇编语言或者C语言程序使用的一组工具,就应该使用.globl命令声明每个函数标签。

汇编程序的模板应该类型如下形式:

.section .data
< initialized data here >
.section .bss
< uninitialized data here >
.section .text
.globl _start
_start:
< instruction code goes here >

创建简单程序

创建一个程序以说明上面介绍的几个部分是如何结合在一起工作的。使用CPUID指令进行介绍。

CPUID指令

CPUID指令请求处理器特定信息并将信息返回到特定寄存器中。使用EAX寄存器作为输入。

EAX值 CPUID输出
0 厂商ID字符串和支持的最大CPUID选项值
1 处理器类型、系列、型号和分步信息
2 处理器缓存配置
3 处理器序列号
4 缓存配置(线程数量、核心数量和牧师引擎)
5 监视信息
80000000h 扩展的厂商ID字符串和支持的级别
80000001h 扩展的处理器类型、系列、型号和分步信息
80000002h~80000004h 扩展的处理器名称字符串

本文使用0选项来获取处理器的厂商ID字符串,字符串返回到时EBX、EDX、ECX寄存器中,如下:

  • EBX包含字符串的最低4个字节
  • ECX包含字符串的最高4个字节
  • EDX包含字符串的中间4个字节

字符串值按照小端(little-endian)格式存放在寄存器中。

CPUID返回值

范例程序

程序如下:

#cpuid.s - By furzoom @ Feb 27, 2015
.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

程序中使用了很多汇编语言命令,不过请将注意力集中在如何在程序中安排指令、指令如何操作的流程及源代码文件如何转换为可执行文件上。

首先,在数据段声明一个字符串:

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

.ascii声明使用ASCII字符声明一个文本字符串。字符串元素被预定义并且放在内存中,其起始内存位置为标签output指示。后面引号中的x作为点位符使用,程序获得的处理器厂商ID将会把它替换掉。

接下来声明程序的指令码段和一般的起始标签:

.section .text
.globl _start
_start:

接着程序将寄存器EAX中加载零值,并运行CPUID指令:

movl $0, %eax
cpuid

CPUID指令运行后,处理返回的3个值:

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

output标签的内存位置加载到EDI寄存器中。然后包含处理器厂商ID字符串的3个寄存器的内容放到数据内存的正确位置。括号外的数字表示相对于output标签的偏移位置。这个数字与EDI寄存器地址相加,确定写入位置。

接着,显示信息:

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

此部分使用Linux的系统调用从Linux内核访问控制台显示。int $0x80使用int指令,生成具有0x80的软件中断。执行的具体函数由EAX寄存器的值来确定。

剩余部分的代码表示退出当前程序,将EBX中的值返回给shell

构建可执行程序

使用GNU汇编器和GNU连接器,方法如下:

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

如果有错误,会有错误提示。如果一切顺利,应该有如下文件:

$ ls -l cpuid*
-rwxr-xr-x 1 mn mn 663 Feb 12 05:23 cpuid
-rw-r--r-- 1 mn mn 648 Feb 12 05:23 cpuid.o
-rw-r--r-- 1 mn mn 390 Feb 12 05:23 cpuid.s

运行可执行程序

$ ./cpuid
The processor vendor ID is 'GenuineIntel'

如果CPU不是Intel的,可能会是其他的结果。无论是什么,占位符已经被替换成CPUID的返回信息了。

使用编译器进行汇编

因为GNU的通用编译器(GNU Common Compiler, gcc)使用GNU汇编器编译C代码,它可以在一步内汇编和连接汇编语言程序。

注意:GNU连接器默认查找_start标签以确定程序开始位置,而gcc查找的是main标签,故需要将程序中的_start标签改为main标签。

.section .text
.globl main
main:

改动后,使用gcc汇编如下:

$ gcc -o cpuid cpuid.s
$ ./cpuid
The processor vendor ID is 'GenuineIntel'

调试程序

在复杂的程序中,给寄存器和内存位置赋值或者试图使用特定指令码处理复杂数据事务时,很容易犯错误。可以使用调试器对程序进行调试。调试主要监视寄存器和内存位置数据是如何变化的。

为了调试汇编语言程序,需要在汇编时使用-gstabs参数:

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

使用-gstabs参数将在可执行程序中添加附加信息,所产生的文件要大一些。不使用-gstabs参数时:

-rwxr-xr-x 1 mn mn 663 Feb 12 05:23 cpuid

使用-gstabs参数后,可执行程序变为:

-rwxr-xr-x 1 mn mn 991 Feb 12 05:23 cpuid

所有如果不是调试,不要使用-gstabs参数进行汇编。

单步运行程序

启动cpuid的调试,首先使用gdb加载要调试的程序:

[mn@furzoom asm]$ gdb -q cpuid
Reading symbols from /home/mn/Desktop/Documents/asm/cpuid...done.
(gdb) 

接着启动程序:

(gdb) run
Starting program: /home/mn/Desktop/Documents/asm/cpuid
The processor Vendor ID is 'GenuineIntel'
(gdb)

从输出结果上看,可能看到其输出与直接执行的输出是一致的。

但是,希望程序启动后停止执行,按照需要一步一步的执行。为了达到这样的目的,必须设置断点(breakpoint)。断点设置在程序代码中希望调试器停止运行程序并查看运行情况的位置。可以在下述位置设置断点:

  • 到达某个标签
  • 到达源代码中的某个行号
  • 数据值到达特定的值时
  • 函数执行了指定的次数之后

在本文的例子中,希望程序一开始运行就停止程序的运行,即需要将断点设置的程序的开始处,也就是_start标签处。gdb使用break设置断点,命令格式为:

break *label+offset

其中label是源代码中的标签,offset是断点距离该标签的行数。如:

(gdb) break *_start
Note: breakpoint -1 also set at pc 0x8048074.
Breakpoint 1 at 0x8048074: file cpuid.s, line 8.
(gdb) run
Starting program: /home/mn/Desktop/Documents/asm/cpuid
The processor Vendor ID is 'GenuineIntel'

Program exited normally.
(gdb)

这里使用*_start参数指定了断点,将断点设置在_start标签后面的第一条指令码处。不幸的是,当程序运行是,并没有停止程序的执行而是直接执行完成。这是gdb中的一个bug。为了解决这个问题,需要在源代码中的_start标签后加一条空指令NOP,空指令就是什么都不作的指令。如:

_start:
nop
movl $0, %eax
cpuid

然后将断点设置在_start+1处,即可正常中断了,如:

(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file cpuid.s, line 9.
(gdb) run
Starting program: /home/mn/Desktop/Documents/asm/cpuid

Breakpoint 1, _start () at cpuid.s:9
9         movl $0, %eax
(gdb)

程序执行后停在了预期的位置,即程序的第9行。可以使用nextstep命令单步调试程序:

(gdb) next
10        cpuid
(gdb) next
11        movl $output, %edi
(gdb) step
12        movl %ebx, 28(%edi)
(gdb) step
13        movl %edx, 32(%edi)
(gdb)

使用nextstep每次只执行一个指令,在查看完需要的程序状态后,使用cont(continue)命令使程序以正常的方式正常继续执行。

(gdb) continue
Continuing.
The processor Vendor ID is 'GenuineIntel'

Program exited normally.
(gdb)

正常继续执行后,输出信息。执行完成后退出。程序单步执行的目的是查看当程序执行到某一步时寄存器和内存位置的数据,下面介绍查看寄存器和内存位置数据的命令。

查看数据

查看寄存器和内存位置数据常用的命令如下:

数据命令 说明
info registers 显示所有寄存器的值
print 显示特定寄存器或者来自程序变量的值
x 显示特定内存位置的内容

使用info registers命令查看寄存器的值十分方便:

(gdb) step
10        cpuid
(gdb) info registers
eax            0x0      0
ecx            0x0      0
edx            0x0      0
ebx            0x0      0
esp            0xbfffe7b0       0xbfffe7b0
ebp            0x0      0x0
esi            0x0      0
edi            0x0      0
eip            0x804807a        0x804807a <_start+6>
eflags         0x200212 [ AF IF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x0      0
(gdb) step
11        movl $output, %edi
(gdb) info registers
eax            0xd      13
ecx            0x6c65746e       1818588270
edx            0x49656e69       1231384169
ebx            0x756e6547       1970169159
esp            0xbfffe7b0       0xbfffe7b0
ebp            0x0      0x0
esi            0x0      0
edi            0x0      0
eip            0x804807c        0x804807c <_start+8>
eflags         0x200212 [ AF IF ID ]
cs             0x73     115
ss             0x7b     123
ds             0x7b     123
es             0x7b     123
fs             0x0      0
gs             0x0      0
(gdb)

明显可以看出,在执行CPUID命令前EBX、ECX、EDX寄存器的值都是0,执行后有相应的值。

print命令用于显示各个寄存器的值:

  • print/d显示十进制的值
  • print/t显示二进制的值
  • print/x显示十六进制的值

如:

(gdb) print/x $ebx
$1 = 0x756e6547
(gdb) print/t $ebx
$2 = 1110101011011100110010101000111
(gdb) print/d $ebx
$3 = 1970169159
(gdb)

x命令用于显示特定内存位置的值,和print命令类似。其格式为

x /nyz

n是要显示的字段数,y是输出格式,可以是:

  • c用于字符
  • d用于十进制
  • x用于十六进制

z表示要显示的字段长度:

  • b用于字节
  • h用于16位(半字)
  • w用于32位
  • g用于64位

如下:

(gdb) x /42cb &output
0x80490ac <output>:     84 'T'  104 'h' 101 'e' 32 ' '  112 'p' 114 'r' 111 'o'99 'c'
0x80490b4 <output+8>:   101 'e' 115 's' 115 's' 111 'o' 114 'r' 32 ' '  86 'V' 101 'e'
0x80490bc <output+16>:  110 'n' 100 'd' 111 'o' 114 'r' 32 ' '  73 'I'  68 'D' 32 ' '
0x80490c4 <output+24>:  105 'i' 115 's' 32 ' '  39 '\'' 71 'G'  101 'e' 110 'n'117 'u'
0x80490cc <output+32>:  105 'i' 110 'n' 101 'e' 73 'I'  110 'n' 116 't' 101 'e'108 'l'
0x80490d4 <output+40>:  39 '\'' 10 '\n'
(gdb)

上面以字符的形式显示output变量的前42个字节。

使用C库函数

在汇编语言程序中还可以很方便的使用标准C语言库函数,下面使用C语言的printf函数输出信息。

使用printf函数

修改程序如下:

#cpuid2.s - By furzoom @ Mar 1, 2015
.section .data
output:
.asciz "The processor vendor ID is '%s'\n"
.section .bss
.lcomm buffer, 12
.section .text
.globl _start
_start:
nop
movl $0, %eax
cpuid
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
pushl $buffer
pushl $output
call printf
addl $8, %esp

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

printf函数使用多个输入参数,这些参数取决于要显示的变量,第一个参数是要输出的字符串。

output:
.asciz "The processor vendor ID is '%s'\n"

.asciz命令表示定义的字符串末尾添加空字符,这是C语言字符串结束方式。

接着在bss段使用.lcomm声明12个字节长度的缓冲区,名字中buffer

.section .bss
.lcomm buffer, 12

CPUID命令执行后,将返回结果放到buffer变量中。

为了把参数传递给C函数的printf,必须把它们压入堆栈。使用PUSHL命令完成,参数放入堆栈的顺序和函数使用它们的顺序是相反的,然后使用CALL命令调用printf函数。然后使用ADDL命令将堆栈中的函数调用参数清除。

pushl $buffer
pushl $output
call printf
addl $8, %esp

连接C库函数

使用C库函数时,汇编程序正常执行,但在连接时,这里使用动态连接(dynamic linking)的方式。在连接时使用-dynamic-linker参数指名程序运行时使用的动态加载程序/lib/ld-linux.so.2。使用-lc指定动态连接库,为/lib/libc.so文件。

$ as -o cpuid2.o cpuid2.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid2 -lc cpuid2.o
$

然后执行程序,得到如下结果:

$ ./cpuid2
The processor vendor ID is 'GenuineIntel'
$

至此,完成了汇编语言对C库函数的调用。

(完)

如无特别说明,本站文章皆为原创,若要转载,务必请注明以下原文信息:
日志标题:《Linux汇编之程序范例》
日志链接:http://furzoom.com/linux-asm-template/
博客名称:枫竹梦

发表评论

插入图片

NOTICE1:请申请gravatar头像,没有头像的评论可能不会被回复!

回到顶部