定义sections
GNU 汇编程序使用 .section 声明语句声明section。.section 语句接受一个参数,即它声明的节的类型。图 4-1 显示了汇编语言程序的布局。
图 4-1 演示了这些段在程序中的正常放置方式。bss部分应该始终放在text section之前,但data section可以移动到text section之后,虽然这不是标准。除了功能之外,您的汇编语言程序还应该
也易于阅读。将所有数据定义放在源代码的开头使其他程序员更容易拿起你的工作并理解它。
定义入口
当汇编语言程序转换为可执行文件时,链接器必须知道您的指令代码中的起点是什么。对于只有一条指令路径的简单程序,找到起点通常不是问题。然而,在使用分散在整个源代码中的多个函数的更复杂的程序中,找到程序的开始位置可能是一个问题。
为了解决这个问题,GNU 汇编程序声明了一个默认标签或标识符,它应该用于应用程序的入口点。_start 标签用于指示程序应该从哪条指令开始运行。如果链接器找不到这个标签,它会产生一个错误信息:
$ ld -o badtest badtest.o ld: warning: cannot find entry symbol _start; defaulting to 08048074 $
从链接器输出中可以看出,如果链接器找不到 _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 指令是一种不容易从高级语言应用程序执行的汇编语言指令。它是一种低级指令,用于查询处理器的特定信息,并返回特定寄存器中的信息。
CPUID 指令使用单个寄存器值作为输入。EAX 寄存器用于确定 CPUID 指令产生的信息。根据 EAX 寄存器的值,CPUID 指令将在 EBX、ECX 和 EDX 寄存器中产生有关处理器的不同信息。
信息以一系列位值和标志的形式返回,必须将其解释为正确的含义。
下表显示了可用于 CPUID 指令的不同输出选项。
本章中创建的示例程序使用零选项从处理器中检索简单的供应商 ID 字符串。当将零值放入 EAX 寄存器并执行 CPUID 指令时,处理器会在 EBX、EDX 和 ECX 寄存器中返回供应商 ID 字符串,如下所示:
- EBX 包含字符串的低 4 字节。
- EDX 包含字符串的中间 4 个字节。
- ECX 包含字符串的最后 4 个字节。
字符串值以小端格式存放在寄存器中; 因此,字符串的第一部分放在寄存器的低位。图 4-2 显示了这是如何工作的。
代码
# cpuid.s Sample program to extract the processor Vendor ID # $ as -o cpuid.o cpuid.s # $ ld -o cpuid cpuid.o .section .data # 数据段 output: # 声明了一个字符串的值(变量) .ascii "The processor Vendor ID is 'xxxxxxxxxxxx'\n" # .ascii指令声明了一段ascii类型的文本,地址就是output,字符串中的x是占位符 .section .text # 代码段 .globl _start # _start 要是让别人调用则要用.globl修饰 _start: # 程序的入口 movl $0, %eax # 将eax赋值为0,0表明了CPUID指令的输出选项,相当于CPUID的参数 cpuid # 调用cpuid指令 # 下面四句是将响应放到output中,因为输出分布在三个寄存器中 # 第一条指令创建一个指针,用于处理在内存中声明的输出变量。 # 输出标签的内存位置被加载到 EDI 寄存器中。 # 接下来,根据 EDI 指针,将包含 Vendor ID 字符串片段的三个寄存器的内容放置在数据存储器中的适当位置。 # 括号外的数字表示相对于放置数据的输出标签的位置。 # 该数字与 EDI 寄存器中的地址相加,以确定寄存器值写入的地址。 # 此过程将用作占位符的 x 替换为实际的供应商 ID 字符串片段 # (请注意,供应商 ID 字符串以奇怪的顺序分为寄存器 EBX、EDX 和 ECX)。 movl $output, %edi # movl %ebx, 28(%edi) movl %edx, 32(%edi) movl %ecx, 36(%edi) # 当所有 Vendor ID 字符串片段都放入内存后,就可以显示信息了: # 该程序使用 Linux 系统调用 (int $0x80) 从 Linux 内核访问控制台显示。 # Linux 内核提供了许多可以从汇编应用程序轻松访问的预设功能。 # 要访问这些内核函数,您必须使用 int 指令代码,它会生成一个值为 0x80 的软件中断。 # 执行的具体功能由 EAX 寄存器的值决定。 # 如果没有这个内核函数,您将不得不自己将每个输出字符发送到显示器的正确 I/O 地址。 # Linux 系统调用可以为汇编语言程序员节省大量时间。 # Linux write 系统调用用于将字节写入文件。以下是写入参数 # 系统调用: # EAX 包含系统调用值。 # EBX 包含要写入的文件描述符。 # ECX 包含字符串的开始。 # EDX 包含字符串的长度。 movl $4, %eax movl $1, %ebx movl $output, %ecx movl $42, %edx int $0x80 # 显示供应商 ID 信息后,就可以干净地退出程序了。 # 同样,Linux 系统调用可以提供帮助。 # 通过使用系统调用 1(exit 函数),程序正确终止,并返回到命令提示符。 # EBX 寄存器包含程序返回给 shell 的退出代码值。 # 这可用于在 shell 脚本程序中产生不同的结果,具体取决于汇编语言程序中的情况。 # 值为零表示程序成功执行。 movl $1, %eax movl $0, %ebx int $0x80
构建可执行文件
将汇编语言源代码程序保存为 cpuid.s,您可以使用 GNU 汇编器和 GNU 链接器构建可执行程序,如下所示:
$ as -o cpuid.o cpuid.s $ ld -o cpuid cpuid.o $
使用 gcc 汇编程序时出现一个问题。GNU 链接器寻找 _start 标签以确定程序的开始,而 gcc 寻找 main 标签(您可能从 C 或 C++ 编程中认出这一点)。您必须更改程序中的 _start 标签和定义标签的 .globl 指令,如下所示:
.section .text .globl main main:
这样做之后,组装和链接程序就变得轻而易举了:
$ gcc -o cpuid cpuid.s $ ./cpuid The processor Vendor ID is ‘GenuineIntel’ $
文章评论