上一篇介绍了C风格的汇编函数,这种风格的汇编函数将数据保存在栈里,栈可以看作是函数独立的私有空间,这种私有性和独立性,让这些汇编函数可以写入单独的汇编文件里,当主程式需要时...

    本文由zengl.com站长对
    http://pan.baidu.com/share/link?shareid=3717576860&uk=940392313 汇编教程英文版相应章节进行翻译得来。

    另外再附加一个英特尔英文手册的共享链接地址:
    http://pan.baidu.com/share/link?shareid=2345340326&uk=940392313 (在某些例子中会用到)

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

Using Separate Function Files 将函数写入单独的文件:

    上一篇介绍了C风格的汇编函数,这种风格的汇编函数将数据保存在栈里,栈可以看作是函数独立的私有空间,这种私有性和独立性,让这些汇编函数可以写入单独的汇编文件里,当主程式需要时,只需通过链接器将这些单独的汇编函数模块链接进来即可,这种模块化思想,在开发大型项目时经常用到。

Creating a separate function file 创建一个汇编函数文件:

    汇编函数文件的结构和主程式类似,为了让文件里定义的函数能被外部的程序访问调用,我们需要用globl伪指令将函数名声明为全局标签,如下面的代码片段:

.section .text
.type area, @function
.globl area
area:

    上面代码通过.globl area伪指令将area函数名声明为全局标签,完整的代码如下:

# area.s - The area function
.section .text
.type area, @function
.globl area
area:
    pushl %ebp
    movl %esp, %ebp
    subl $4, %esp
    fldpi
    filds 8(%ebp)
    fmul %st(0), %st(0)
    fmulp %st(0), %st(1)
    fstps -4(%ebp)
    movl -4(%ebp), %eax
    movl %ebp, %esp
    popl %ebp
    ret
 

    上面的代码其实就是将上一节functest3.s里的area函数放置到单独的area.s文件里了,函数的主体代码都是一样的,不同之处在于area.s文件里使用了.globl area来声明area函数,下面通过functest4.s程式来演示外部程序是如何调用area.s里的area函数的:

# functest4.s - An example of using external functions
.section .data
precision:
    .byte 0x7f, 0x00
.section .bss
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    finit
    fldcw precision
    pushl $10
    call area
    addl $4, %esp
    movl %eax, result
    pushl $2
    call area
    addl $4, %esp
    movl %eax, result
    pushl $120
    call area
    addl $4, %esp
    movl %eax, result
    movl $1, %eax
    movl $0, %ebx
    int $0x80
 

    上面的functest4.s和上一节functest3.s的唯一区别在于:functest4.s将area函数移到area.s文件里了,剩余的代码和functest3.s完全一样。

    接下来,我们看下如何由functest4.s和area.s来创建最终的可执行文件。

Creating the executable file 创建最终的可执行文件:

    第一步是使用gas汇编器分别生成functest4.s和area.s的目标文件:

$ as -gstabs -o area.o area.s
$ as -gstabs -o functest4.o functest4.s

$

    接下来就是使用链接器生成可执行文件,如果你只链接functest4.o一个文件的话,就会出现如下错误:

$ ld -o functest4 functest4.o
functest4.o:functest4.s:14: undefined reference to `area'
functest4.o:functest4.s:18: undefined reference to `area'
functest4.o:functest4.s:22: undefined reference to `area'
$

    上面出现的undefined reference to `area'的错误,说明链接器无法解析area函数,这是因为functest4.o里并没有area函数的具体定义代码,area函数的实体代码是位于area.o里的,所以正确的链接命令应该是:

$ ld -o functest4 functest4.o area.o
$

    现在就可以使用gdb调试器对生成的functest4进行调试了,调试的结果和前一章functest3的例子一样。

Debugging separate function files 调试汇编函数文件:

    从上面gas生成functest4.o和area.o的命令可以看到,它们都指定了一个-gstabs的调试参数,这样当gdb在functest4里遇到call area指令时,只要使用s单步步入命令就可以进入area.s里的area函数进行调试,但是在某些大型项目里,你可能对某些汇编函数文件不感兴趣,不想每次使用s命令时都自动进入这些函数文件的话,就可以考虑将这些文件在汇编时的-gstabs参数给去掉,例如下面的例子就只对functest4.s主程式使用了-gstabs参数,而area.s则没有使用该参数:

$ as -o area.o area.s
$ as -gstabs -o functest4.o functest4.s
$ ld -o functest4 functest4.o area.o
$ gdb -q functest4

Reading symbols from /home/zengl/Downloads/asm_example/func/functest4...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file functest4.s, line 11.
(gdb) r
Starting program: /home/zengl/Downloads/asm_example/func/functest4

Breakpoint 1, _start () at functest4.s:11
11        finit

(gdb) s
12        fldcw precision
(gdb) s
13        pushl $10
(gdb) s
14        call area
(gdb) s
15        addl $4, %esp
(gdb) print/f $eax
$1 = 314.159271
(gdb)

    从上面输出可以看到,由于生成area.o时没有使用-gstabs参数,所以area.o里就没有调试信息,这样在s命令单步执行call area指令时,就不会进入area函数了,并且这并不影响area函数的运行结果,EAX里的314.159271的结果和预期的一致。

Using Command-Line Parameters 使用命令行参数:

    除了可以通过栈来传递函数的参数外,还可以通过栈来获取到用户在命令行上输入的参数。

The anatomy of a program 程序的内存结构分析:

    不同的操作系统向程序传递命令行参数的方法不同,要了解Linux系统下,命令行参数是如何传递给程序的,就必须首先了解程序在Linux下运行时的内存结构。

    当程序从Linux的Shell终端开始运行时,Linux系统会为程序分配一段内存空间,这段内存在实际的物理内存里的起始位置是不确定的,但是Linux系统在保护模式下,开启了虚拟内存管理机制,这套机制让每个运行的用户程序具有相同的虚拟内存地址空间,这些虚拟内存地址会被系统自动映射为具体的物理内存地址,所以对于用户程序而言,在指令里所用到的内存地址都是虚拟内存地址,具体的物理内存对用户而言是透明的,这样就可以简化程序的开发。

    那么,在Linux系统里,每个用户程序运行时的起始虚拟内存地址都是0x8048000,结束地址是0xbfffffff,用户程序在这段虚拟内存空间里的结构如下图所示:

图1

    上图从0x8048000开始的第一块内存区域包含了汇编程序所有的指令和数据部分(包括.bss和.data段)。该部分的指令代码既含有你自己写的代码,也含有Linux系统链接程序为了能让你的程序正常运行而加进来的必要指令信息。

    程序的最后一块内存区域是Stack Data数据栈部分,程序在刚开始运行时,ESP栈顶指针指向的位置是0xbfffffff,但是在Linux系统将控制权交由你的程序之前,它还会向栈里压入一些数据,比如用户在命令行输入的参数,以及系统的环境变量等,所以当你的程序的第一条指令运行时,ESP的值会小于0xbfffffff。

Analyzing the stack 分析栈结构:

    Linux系统在程序启动时会向栈里压入以下4种类型的数据:
  • The number of command-line parameters (including the program name)
    命令行参数的个数(该个数包括程序名在内)
  • The name of the program as executed from the shell prompt
    从shell终端运行的程序名称
  • Any command-line parameters included on the command line
    命令行里包含的所有参数
  • All the current Linux environment variables at the start of the program
    程序执行时所有的Linux环境变量
    这些栈里的程序名称,命令行参数以及环境变量都是以零结尾的字符串,并且都有相应的指针指向这些字符串,程序启动时,栈里的完整结构如下图所示:

图2

    ESP指向的是系统最后压入栈的Number of Parameters(命令行参数个数),该参数个数包括程序名在内,也就是说,当用户没有给程序提供任何命令行参数时,该参数个数也会是1,因为程序名也算在个数里。

    在命令行参数个数之前压入栈的是Program Name(程序名称),这是一个4字节的指针值,指向程序名对应的字符串。

    再接下来,就是Pointer to Command Line ...部分,该部分是用户输入的命令行参数部分,这些也都是4字节的字符串指针值。再往后,就是指向环境变量的指针,最后就是存放具体的命令行参数和环境变量的字符串部分。

    下面通过gdb调试运行functest4来分析栈的存储情况:

$ gdb -q functest4
Reading symbols from /home/zengl/Downloads/asm_example/func/functest4...done.
(gdb) break *_start+1
Breakpoint 1 at 0x8048075: file functest4.s, line 11.
(gdb) run 10
Starting program: /home/zengl/Downloads/asm_example/func/functest4 10

Breakpoint 1, _start () at functest4.s:11
11        finit

(gdb) print $esp
$1 = (void *) 0xbffff380
(gdb)

    上面在gdb提示符下,通过run 10命令来运行functest4程序,同时向该程序传递一个10的命令行参数,在执行第一条具体的指令代码finit之前,ESP指向的栈顶位置为0xbffff380,接下来就可以使用x命令来查看ESP指向的内存里的值:

(gdb) x/20x 0xbffff380
0xbffff380:	0x00000002	0xbffff4ff	0xbffff530	0x00000000
0xbffff390:	0xbffff533	0xbffff546	0xbffff55a	0xbffff570
0xbffff3a0:	0xbffff580	0xbffff58b	0xbffff5dc	0xbffff5ee
0xbffff3b0:	0xbffff618	0xbffff623	0xbffffb44	0xbffffb58
0xbffff3c0:	0xbffffb92	0xbffffbc6	0xbffffbf5	0xbffffc2e
(gdb) 

    上面通过x/20x命令来查看ESP指向的0xbffff380开始的连续20个双字(一个双字对应4个字节)里的值,可以看到最后一个压入栈的值0x00000002表示命令行的参数个数为2,该个数包括了程序名在内,接下来的0xbffff4ff和0xbffff530分别为程序名和命令行参数的字符串的指针值,同样可以使用x命令来查看:

(gdb) x/s 0xbffff4ff
0xbffff4ff:     "/home/zengl/Downloads/asm_example/func/functest4"
(gdb) x/s 0xbffff530
0xbffff530:     "10"
(gdb)

    从上面输出可以看到,0xbffff4ff指针指向的字符串为"/home/zengl/Downloads/asm_example/func/functest4",该字符串就是包括当前运行的程序名在内的完整路径,0xbffff530指向的"10"则是前面run命令设置的命令行参数。

    值得注意的是:所有的命令行参数都是以字符串的形式存储在栈里的,像上面例子里的10,即便它看起来像整数,但也是以字符串的形式传递给程序的。

    在命令行参数指针0xbffff530的后面是0x00000000的空指针,它用于将前面的命令行参数和后面的环境变量给分隔开。

    从0xbffff390开始的栈里的数据就是系统压入栈的环境变量的字符串指针值,也可以用x命令来查看这些指针对应的字符串:

(gdb) x/s 0xbffff533
0xbffff533:     "SSH_AGENT_PID=2038"
(gdb) x/s 0xbffff546
0xbffff546:     "GLADE_PIXMAP_PATH=:"
(gdb) x/s 0xbffff55a
0xbffff55a:     "XDG_MENU_PREFIX=xfce-"
(gdb) x/s 0xbffff570
0xbffff570:     "SHELL=/bin/bash"
(gdb) x/s 0xbffff580
0xbffff580:     "TERM=xterm"
(gdb) x/s 0xbffff58b
0xbffff58b:     "XDG_SESSION_COOKIE=6e570ba4a0926eac818322d100000008-1394002931.742425-1957305930"
(gdb)

    上面显示的就是当前程序运行时,系统压入栈的环境变量,在不同的系统环境和不同的系统配置下,这些值都可能会不一样。

Viewing command-line parameters 编写程序来查看命令行参数:

    现在我们知道了命令行参数在栈里的存储情况,就可以在程序里访问和显示这些参数,下面的paramtest1.s程式就演示了如何访问命令行参数:

# paramtest1.s - Listing command line parameters
.section .data
output1:
    .asciz "There are %d parameters:\n"
output2:
    .asciz "%s\n"
.section .text
.globl _start
_start:
    nop
    movl (%esp), %ecx
    pushl %ecx
    pushl $output1
    call printf
    addl $4, %esp
    popl %ecx
    movl %esp, %ebp
    addl $4, %ebp
loop1:
    pushl %ecx
    pushl (%ebp)
    pushl $output2
    call printf
    addl $8, %esp
    popl %ecx
    addl $4, %ebp
    loop loop1
    pushl $0
    call exit
 

    上面的代码先用movl (%esp), %ecx指令,通过间接寻址的方式,将ESP栈顶指针指向的命令行参数个数传递给ECX,然后将ECX和output1格式化字符串的指针值作为参数依次压入栈,接着就可以通过printf函数将ECX里的参数个数给显示出来。

    在call printf输出显示了参数个数后,先用addl $4, %esp将之前pushl $output1压入栈的格式化字符串的指针参数给丢弃掉,再通过popl %ecx将之前压入栈的ECX参数个数弹出恢复到ECX寄存器,此时ESP就指向程序刚开始运行时的内存位置,接下来,为了能用EBP来循环访问到每一个命令行参数,就用movl %esp, %ebp将ESP赋值给EBP,此时的EBP指向的是栈里存储命令行参数个数的位置,所以需要addl $4, %ebp指令将EBP加4字节,从而让EBP指向程序名的字符串指针。

    在loop1循环里,会根据ECX里的命令行参数个数作为计数器,EBP作为参数的指针,循环将所有命令行参数(包括程序名在内)都通过printf函数输出显示出来,在循环体里,每输出显示一个命令行参数后,就会用addl $4, %ebp将EBP加4,从而指向下一个要显示的参数指针,同时loop loop1指令会在内部将ECX减一,减一后如果ECX为0,则跳出循环。最后通过exit的C标准库函数来退出程序。

    paramtest1.s程式在汇编链接后,运行结果如下(由于使用了C标准库函数,所以ld链接时使用了-dynamic-linker /lib/ld-linux.so.2 -lc参数来指定动态库的加载器和对应的C库):

$ as -gstabs -o paramtest1.o paramtest1.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o paramtest1 paramtest1.o
$ ./paramtest1 testing 1 2 3 testing

There are 6 parameters:
./paramtest1
testing
1
2
3
testing

$

    上面输出显示一共有6个参数(包括程序名在内),然后循环将程序名和所有的命令行参数给显示出来。

    需要注意的是:由于程序名也包含在参数个数里,所以参数个数最少为1。

Viewing environment variables 通过程序查看环境变量:

    既然可以查看命令行参数,那么同理,也可以通过指针和循环来查看所有存储在栈里的系统环境变量,下面的paramtest2.s程式就演示了这一做法:

# paramtest2.s - Listing system environment variables
.section .data
output:
    .asciz "%s\n"
.section .text
.globl _start
_start:
    nop
    movl %esp, %ebp
    addl $12, %ebp
loop1:
    cmpl $0, (%ebp)
    je endit
    pushl (%ebp)
    pushl $output
    call printf
    addl $8, %esp
    addl $4, %ebp
    loop loop1
endit:
    pushl $0
    call exit
 

    上面的代码先用movl %esp, %ebp指令将ESP赋值给EBP,然后通过addl $12, %ebp指令将EBP加12,从而让EBP跳过栈里的参数个数,程序名和起分隔作用的空指针,并定位到第一个环境变量的字符串指针位置。接着就可以通过loop1循环体将所有的系统环境变量通过printf函数给显示出来,在loop1的开头,通过cmpl $0, (%ebp)指令将EBP指向的环境变量的字符串指针和0即空指针进行比较,这是因为环境变量部分是以空指针结束的,所以当EBP指向了空指针时,就说明所有的环境变量都显示完了,就可以通过je endit指令跳转到endit标签处,通过exit函数来退出程序。

    在汇编链接后,程序的运行情况如下:

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

SSH_AGENT_PID=2038
GLADE_PIXMAP_PATH=:
TERM=xterm
SHELL=/bin/bash
XDG_MENU_PREFIX=xfce-

....................... //省略部分输出
HOME=/home/zengl
LANGUAGE=en
LOGNAME=zengl
XDG_DATA_DIRS=/usr/share/ubuntustudio:/usr/local/share/:/usr/share/:/usr/share
DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-PyOuBSVjoC,guid=ceabd9769061d68fc3721f2000000033
LESSOPEN=| /usr/bin/lesspipe %s
DISPLAY=:0.0
GLADE_CATALOG_PATH=:
LIBGLADE_MODULE_PATH=:
XDG_CURRENT_DESKTOP=XFCE
LESSCLOSE=/usr/bin/lesspipe %s %s
COLORTERM=Terminal
XAUTHORITY=/home/zengl/.Xauthority
OLDPWD=/home/zengl
_=./paramtest2

$

    以上是我的系统里的系统环境变量的输出情况,不同的系统和不同的本地配置,输出情况也会不同。

    此外,你还可以设置自己的环境变量,然后进行类似如下的测试:

$ TESTING=/home/rich ; export TESTING
$ ./paramtest2

.....................
.....................
.....................

TESTING=/home/rich
XDG_CURRENT_DESKTOP=XFCE
LESSCLOSE=/usr/bin/lesspipe %s %s
COLORTERM=Terminal
XAUTHORITY=/home/zengl/.Xauthority
_=./paramtest2
$

    从上面输出可以看到,在用export命令导出了自己设置的TESTING环境变量后,再次运行paramtest2程式,就可以在输出结果里看到TESTING=/home/rich的环境变量了。

An example using command-line parameters 使用命令行参数的例子:

    从前面的介绍中可知,命令行参数都是以字符串的形式存储在栈里的,所以如果你想以整数的形式使用这些参数的话,就需要自己做些转化工作,有很多的方法可以将字符串转为整数或浮点数,这里介绍一种简单的直接使用C库函数的方法,有如下三个可用于转换的C库函数:
  • atoi(): Converts an ASCII string to a short integer value
    atoi函数:将ASCII编码的字符串转为一个32位的整数值
  • atol(): Converts an ASCII string to a long integer value
    atol函数:将ASCII编码的字符串转为一个64位的整数值
  • atof(): Converts an ASCII string to a double-precision floating-point value
    atof函数:将ASCII编码的字符串转为一个双精度的浮点数
   上面这些函数都需要接受一个用于转换的字符串指针值,atoi函数会将转换的结果存储到EAX寄存器,atol函数的结果则存储在EDX:EAX的寄存器对里(因为结果是个64位的整数值),atof函数的结果则存储在FPU的ST0寄存器里。

    下面的paramtest3.s程式就演示了如何从命令行读取一个参数,并将该参数转为整数,再使用该整数来计算出对应的圆面积的过程:

# paramtest3.s - An example of using command line parameters
.section .data
output:
    .asciz "The area is: %f\n"
.section .bss
    .lcomm result, 4
.section .text
.globl _start
_start:
    nop
    finit
    pushl 8(%esp)
    call atoi
    addl $4, %esp
    movl %eax, result
    fldpi
    filds result
    fmul %st(0), %st(0)
    fmul %st(1), %st(0)
    fstpl (%esp)
    pushl $output
    call printf
    addl $12, %esp
    pushl $0
    call exit
 

    上面的代码在finit初始化FPU后,先通过pushl 8(%esp)将第一个命令行参数的字符串指针压入栈,作为下一步atoi函数的参数,接着通过call atoi指令调用atoi函数将命令行参数由字符串转为整数,结果存储在EAX里,然后由movl %eax, result指令将整数结果存储到result内存位置,作为后面计算圆面积的半径值,在fldpi加载pi到FPU寄存器栈后,再由filds result将转换的整数半径值压入FPU的ST0里,之前压入的pi则移入ST1,通过fmul %st(0), %st(0)指令可以将ST0里的整数半径值开平方,平方结果还是存储在ST0,随后的fmul %st(1), %st(0)就可以将ST0里半径的平方和ST1里的pi值相乘,这样就可以得到对应的圆面积,并存储在ST0里,为了将该面积值显示出来,代码里使用fstpl (%esp)指令将ST0里的双精度的面积值弹出到ESP指向的栈顶位置,从而可以作为printf函数的参数,最后由printf函数将计算得到的面积值给输出显示出来。

    在汇编链接后,就可以使用不同的命令行参数来进行测试,具体的运行情况如下:

$ as -gstabs -o paramtest3.o paramtest3.s
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o paramtest3 paramtest3.o
$ ./paramtest3 10

The area is: 314.159265
$ ./paramtest3 2
The area is: 12.566371
$ ./paramtest3 120
The area is: 45238.934212
$

    可以看到paramtest3程式将输入的命令行参数10,2,120作为半径值,依次计算并输出了对应的圆面积,这些输出结果和预期的一致。

    最后就是英文原著第11章的总结部分,限于篇幅就不多说了,下一篇开始介绍Linux里和系统调用相关的内容。

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

下一篇: 汇编里使用Linux系统调用 (一)

上一篇: 汇编函数的定义和使用 (二)

相关文章

使用IA-32平台提供的高级功能 (三) SSE相关指令

汇编流程控制 (二) 条件分支及汇编循环

IA-32平台(三) 硬件介绍结束篇及各种处理器平台

汇编字符串操作 (一)

基本数学运算 (四) 基本运算结束篇

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