阅读(2781) (1)

Assembly创建一个程序的步骤

2016-10-26 10:54:43 更新

现今,完全用汇编语言写的独立的程序是不经常的。汇编一般用在某些至关重要的程序。为什么?用高级语言来编程比用汇编要简单得多。同样,使用汇编将使得程序移植到另一个平台非常困难。事实上,根本很少使用汇编程序。


那么,为什么任何人都需要学习汇编程序呢?


1. 有时,用编程写的代码比起编译器产生的代码要少而且运行得更快。


2. 汇编允许直接访问系统硬件信息,而这个在高级语言中很难或根本不可能实现。


3. 学习汇编编程将帮助一个人深刻地理解系统如何运行。


4. 学习汇编编程将帮助一个人更好地理解编译器和高级语言像C如何工作。


这两点表明学习汇编是非常有用的,即使在以后的日子里不在编程里用到它。事实上,作者很少用汇编编程,但是他每天都使用来自它的想法。


第一个程序

在这一节里的初期的程序将全部从图 1.6 里的简单 C 驱动程序开始。它简单地调用另一个称为 asm main 的函数。这个是真正意义将用汇编编写的程序。使用C驱动程序有几个优点。首先,这样使C系统正确配置程序在保护模式下运行。所有的段和它们相关的段寄存器将由 C 初始化。汇编代码不需
要为这个担心。其次,C 库同样提供给汇编代码使用。作者的 I/O 程序利用了这个优点。他们使用了 C 的 I/O 函数( printf,等)。下面显示了一个简单的汇编程序。

driver.c代码

___________________________________________________________first.asm___________________________________________________________
1 ; 文件: first.asm
2 ; 第一个汇编程序。这个程序总共需要两个整形变量作为输入然后输出它们的和。
3 ;
4 ;
5 ; 利用djgpp 创建执行文件:
6 ; nasm -f coff first.asm
7 ; gcc -o first first.o driver.c asm_io.o
8
9 %include "asm_io.inc"
10 ;
11 ; 初始化放入到数据段里的数据
12 ;
13 segment .data
14 ;
15 ; 这些变量指向用来输出的字符串
16 ;
17 prompt1 db     "Enter a number: ", 0 ; 不要忘记空结束符
18 prompt2 db     "Enter another number: ", 0
19 outmsg1 db     "You entered ", 0
20 outmsg2 db     " and ", 0
21 outmsg3 db     ", the sum of these is ", 0
22
23 ;
24 ; 初始化放入到.bss段里的数据
25 ;
26 segment .bss
27 ;
28 ; 这个变量指向用来储存输入的双字
29 ;
30 input1 resd 1
31 input2 resd 1
32
33 ;
34 ; 代码放入到.text段
35 ;
36 segment .text
37             global _asm_main
38 _asm_main:
39             enter 0,0 ; 开始运行
40             pusha
41
42              mov eax, prompt1 ; 输出提示
43              call print_string
44
45              call read_int ; 读整形变量储存到input1
46              mov [input1], eax ;
47
48              mov eax, prompt2 ; 输出提示
49              call print_string
50
51              call read_int ; 读整形变量储存到input2
52              mov [input2], eax ;
53
54              mov eax, [input1] ; eax = 在input1里的双字
55              add eax, [input2] ; eax = eax + 在input2里的双字
56              mov ebx, eax ; ebx = eax
57
58              dump_regs 1 ; 输出寄存器值
59              dump_mem 2, outmsg1, 1 ; 输出内存
60 ;
61 ; 下面分几步输出结果信息
62 ;
63              mov eax, outmsg1
64              call print_string ; 输出第一条信息
65              mov eax, [input1]
66              call print_int ; 输出input1
67              mov eax, outmsg2
68              call print_string ; 输出第二条信息
69              mov eax, [input2]
70              call print_int ; 输出input2
71              mov eax, outmsg3
72              call print_string ; 输出第三条信息
73              mov eax, ebx
74              call print_int ; 输出总数(ebx)
75              call print_nl ; 换行
76
77              popa
78              mov eax, 0 ; 回到C中
79              leave
80              ret
___________________________________________________________first.asm___________________________________________________________

这个程序的第13行定义了指定储存数据的内存段的部分代码(名称为 .data )。只有是初始化的数据才需定义在这个段中。行 17 到 20,声明了几个字符串。它们将通过 C 库输出,所以必须以 null 字符(ASCII 值为 0)结束。记住 0 和 '0' 有很大的区别。

不初始化的数据需声明在 bss 段(名为 .bss,在 26 行)。这个段的名字来自于早期的基于 UNIX 汇编运算符,意思是\由符号开始的块。"这同样会有一个堆栈段。它将在以后讨论。

代码段根据惯例被命名为.text。它是放置指令的地方。注意主程序(38行)的代码标号有一个下划线前缀。这个是在 C 中称为约定的一部分。这个约定指定了编译代码时 C 使用的规则。C 和汇编交互使用时,知道这个约定是非常重要的。以后将会将全部约定呈现;但是,现在你只需要知道所有的 C 编译器里的 C 符号(也就是: 函数和全局变量)有一个附加的下划线前缀。(这个规定是为 DOS/Windows 指定的,在 linux 下C 编译器并不为 C 符号名上加任何东西。)

在 37 行的全局变量(global)指示符告诉汇编定义 asm main 为全局变量。与 C 不同的是,变量在缺省情况下只能使用在内部范围中。这就意味着只有在同一模块的代码才能使用这个变量。global 指示符使指定的变量可以使用在外部范围中。这种类型的变量可以被程序里的任意模块访问。asm io 模块声明了全局变量 print int 和 et.al.。这就是为什么在 first.asm 模块里能使用它们的缘故。

编译器依赖

上面的汇编代码指定为基于 GNU (GNU 是一个以免费软件为基础的计划 http://www.fsf.org )的 DJGPP C/C++ 编译器 ( http://www.delorie.com/djgpp )。 这个编译器可以从 Internet 上免费下载。它要求一个基于386或更好的PC而且需在DOS,Windows 95/98 或NT下运行。这个编译器使用COFF (CommonObject File Format,普通目标文件格式)格式的目标文件。为了符合这个格
式,nasm 命令使用 -f coff 选项(就像上面代码注释展示的一样)。最终目标文件的扩展名为 o。

Linux C 编译器同样是一个 GNU 编译器。为了转变上面的代码使它能在 Linux 下运行,只需简单将37到38行里的下划线前缀移除。Linux使用ELF(Executable and Linkable Format,可执行和可连接格式)格式的目标文件。Linux 下使用 -f elf 选项。它同样产生一个扩展名为 o 的目标文
件。

Borland C/C++ 是另一个流行的编译器。它使用微软 OMF 格式的目标文件。Borland 编译器使用 -f obj 选项。目标文件的扩展名将会是 obj。OMF 与其它目标文件格式相比使用了不同的段指示符。数据段(13行)必须改成:

        segment DATA public align=4 class=DATA use32
bss 段(26)必须改成:
        segment BSS public align=4 class=BSS use32
text 段(36)必须改成:
        segment TEXT public align=1 class=CODE use32
必须在 36 行之前加上一新行:
        group DGROUP BSS DATA

微软 C/C++ 编译器可以使用 OMF 或 Win32 格式的目标文件。(如果给出的是 OMF 格式,它将从内部把信息转变成 Win32 格式。)Win32 允许像 DJGPP 和 Linux 一样来定义段。在这个模式下使用 -f win32 选项来输出。目标文件的扩展名将会是 obj。

汇编代码

第一步是汇编代码。在命令行,键入:

nasm -f object-format first.asm

object-format要么是coff ,elf ,obj,要么是win32,它由使用的C编译器决定。(记住在Linux和Borland下,资源文件同样必须改变。)

编译C代码

使用C编译器编译 driver.c 文件。对于 DJGPP ,使用:

gcc -c driver.c

-c 选项意味着编译,而不是试图现在连接。同样的选项能使用在 Linux,Borland 和 Microsoft 编译器上。

连接目标文件

连接是一个将在目标文件和库文件里的机器代码和数据结合到一起产生一个可执行文件的过程。就像下面将展示的,这个过程是非常复杂的。C 代码要求运行标准 C 库和特殊的启动代码。与直接调用连接程序相比,C 编译器更容易调用带几个正确的参数的连接程序。

例如:使用 DJGPP 来连接第一个程序的代码,使用:

gcc -o first driver.o first.o asm_io.o

这样产生一个 first.exe(或在 Linux 下只是 first)可执行文件。

对于 Borland,你需要使用:

bcc32 first.obj driver.obj asm_io.obj

Borland 使用列出的第一个文件名来确定可执行文件名。所以在上面的例子里,程序将被命名为 first.exe。

将编译和连接两个步骤结合起来是可能的。例如:

gcc -o first driver.c first.o asm_io.o

现在 gcc 将编译 driver.c 然后连接。

理解一个汇编列表文件

-l listing-file 选项可以用来告诉 nasm 创建一个指定名字的列表文件。这个文件将显示代码如何被汇编。这儿显示了 17 和 18 行(在数据段)在列表文件中如何显示。(行号显示在列表文件中;但是注意在源代码文件中显示的行号可能不同于在列表文件中显示的行号。)

48 00000000 456E7465722061206E-     prompt1 db     "Enter a number: ", 0
49 00000009 756D6265723A2000
50 00000011 456E74657220616E6F-     prompt2 db     "Enter another number: ", 0
51 0000001A 74686572206E756D62-
52 00000023 65723A2000

每一行的头一列是行号,第二列是数据在段里的偏移地址(十六进制显示)。第三列显示将要储存的十六进制值。这种情况下,十六进制数据符合 ASCII 编码。最终,显示来自资源文件的正文。列在第二行的偏移地址非常可能不是数据存放在完成后的程序中的真实偏移地址。每个模块可能在数据段(或其它段)定义它自己的变量。在连接这一步时,所有这些数据段的变量定义结合形成一个数据段。最终的偏移由连接程序计算得到。

这儿有一小部分 text 段代码(资源文件中 54 到 56 行)在列表文件中如何显示:

94 0000002C A1[00000000]                 mov          eax, [input1]
95 00000031 0305[04000000]              add           eax, [input2]
96 00000037 89C3                             mov          ebx, eax

第三列显示了由汇编程序产生的机器代码。通常一个指令的完整代码不能完全计算出来。例如:在 94 行,input1 的偏移(地址)要直到代码连接后才能知道。汇编程序可以算出 mov 指令(在列表中为 A1)的操作码,但是它把偏移写在方括号中,因为准确的值还不能算出来。这种情况下,0 作为一个暂时偏移被使用,因为 input1 在这个文件中,被定义在 bss 段的开始。记住这不意味着它会在程序的最终 bss 段的开始。当代码连接后,连接程序将在位置上插入正确的偏移。其它指令,如 96 行,并不涉及任何变量。这儿汇编程序可以算出完整的机器代码。

Big和Little Endian 表示法

如果有人仔细看过 95 行,将会发现机器代码中的方括号里的偏移地址非常奇怪。input2 变量的偏移地址为4(像文件定义的一样);但是显示在内存里偏移不是00000004,而是04000000。

为什么?不同的处理器在内存里以不同的顺序储存多字节整形:big endian 和 little endian。Big endian 是一种看起来更自然的方法。最大(也就是: 最高有效位)的字节首先被储存,然后才是第二大的,依此类推。

例如:双字 00000004 将被储存为四个字节 00 00 00 04。IBM 主机,许多 RISC 处理器和 Motorola 处理器都使用这种 big endian 方法。然而,基于Intel的处理器使用 little endian 方法!首先被
储存是最小的有效字节。所以 00000004 在内存中储存为 04 00 00 00。这种格式强制连入 CPU 而且不可能更改。通常情况下,程序员并不需要担心使用的是哪种格式。但是,在下面的情况下,它们是非常重要的。

1. 当二进制数据在不同的电脑上传输时(不管来自文件还是网络)。
2. 当二进制数据作为一个多字节整形写入到内存中然后当作单个单个字节读出,反之亦然。

Endian 格式并不应用于数组的排序。数组的第一个元素通常在最低的地址里。这个应用在字符串里(字符数组)。Endian 格式依然用在数组的单个元素中。

骨架文件

图1.7显示了一个可以用来书写汇编程序的开始部分的骨架文件。

骨架程序