AT&T汇编学习总结二-汇编语言程序范例

时间:2022-06-01 14:10:17
第四章汇编语言范例
  1. 创建简单程序

    • CPUID指令:CPUID指令是一条汇编指令,不容易从高级语言应用程序执行它。它是请求处理器的特定信息并且把信息返回到特定寄存器中的低级指令。
    • CPUID指令使用单一的寄存器值作为输入。EAX寄存器用于决定CPUID指令生成什么信息,根据EAX寄存器的值,CPUID指令在EBX和EDX寄存器中生成关于处理器的不同信息。信息以一系列位值和标志的形式返回。

    • CPUID指令可用的不同输出选项:

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

2. 范例程序

  • .ascii声明ASCII字符串声明一个文本字符串,字符串元素被预订以并且放在内存中,其起始内存位置由标签output指示:
output:
.ascii "The processor Vendor ID is 'xxxxxxxxxx'\n"
  • 声明程序的指令码段和一般的起始标签:
.section .text
.globl _start
_start:
  • EAX寄存器加载零值,然后运行CPUID指令:
movl $0, %eax
cpuid
  • EDI是用于字符串操作的目标的数据指针,EAX中的零值定义CPUID输出选项。CPUID运行后,必须收集分散在3个输出寄存器中的指令响应:
movl $output %edi
movl %ebx, 28(%edi)
movl %ebx, 32(%edi)
movl %ebx, 36(%edi)
  • EAX:用于操作数和结果数据的累加器;EBX:指向数据内存段中的数据的指针;EDX:I/O指针;ECX:字符串和循环操作的计数器;EDI:用于字符串操作的目标的数据指针。
    第一条指令创建一个指针,处理内存中声明的output变量时会使用这个指针。output标签的内存位置被加载到EDI寄存器中。括号外的数字表示相对于output标签的放置数据的位置,这个数字和EDI寄存器中的地址相加,确定寄存器的被写入的地址,按照EDI指针、包括厂商ID字符串片段的3个寄存器的内容被放到数据内存中的正确位置:
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
  • 这个程序使用一个Linux系统调用(inti $0x80)从Linux内核访问控制台显示:
movl $4, %eax
movl $1, %ebx
movl $output, %ecx
movl $42, %edx
int $0x80
  1. 构建可执行程序

    • 使用GNU汇编器和GNU连接器构建可执行程序,第一步使用as命令把汇编语言源代码汇编为目标代码文件cpuid.o,第二步使用ld把目标代码文件连接为可执行文件cpuid:
as -o cpuid.o cpuid.s
ld -o cpuid cpuid.o
  1. 运行可执行程序

    • Linux的好处之一是一些版本能够运行在大多数可能已经被放到一边不用的老型号计算机上。
      ./cpuid
  2. 使用汇编器进行汇编

    • gcc汇编程序时有一个问题,GNU连接器查找_start标签以便确定程序的开始的位置,但是gcc查找的是main标签。所以必须把程序中的_start标签和定义标签的.globl命令都改成下面:
.section .txt
.globl main
main:
  1. 调试程序

    • 在更加复杂的程序中,在给寄存器和内存位置赋值或者试图使用特定指令码处理复杂数据事物时,易错误。为调试汇编语言程序,首先必须使用-gstabs参数重新汇编源代码:
    • 因为-gstabs参数在可执行程序文件中添加了附加信息,所以产生的文件比仅仅运行应用程序所需的文件要大一些,所以若非必要就不要使用调试信息
as -gstabs -o cpuid.o cpuid.s
ld -o cpuid cpuid.o
  1. 调试程序之设置断点,在下列任何情况下停止程序的执行:
    • 到达某个标签
    • 到达源代码中的某个行号
    • 函数执行了指定的次数之后
      break命令格式:其中label是被引用的源代码中的标签,offset是执行应该停止的地方距离这个标签的行数。
break * label+offset
**举例:这里使用\*_start参数指定了断点,这个参数指定_start标签后面的第一条指令码,但不幸的是,当程序运行时,它会忽略这个断点并且运行完整个程序。这是gdb当前版本中众知的一个缺点:**
break *_start
  • 解决方法是在*_start标签后的第一个指令码元素的位置包含一条伪指令,在汇编语言中,空指令称为NOP,意思是空操作:
_start:
nop
movl $0, %eax
cpuid

在添加NOP指令后,就可以在这个位置创建断电,表示_start+1

break *_start+1
  1. 调试程序之查看数据
    • 两种最常被检查的数据元素是用于变量的寄存器和内存位置:
数据命令 描述
info registcrs 显示所有寄存器的值
print 显示特定寄存器或者来自程序的变量的值
x 显示特定内存位置的内容

* printf命令也可以用于显示各个寄存器的值,加上一个修饰符就可以修改print命令输出格式:

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

* 使用print命令

print/x $ebx


  • x命令用于显示特定内存位置的值,和print命令类似,可以使用修饰符修改x命令的输出。x命令格式是:
x/nyz
n是显示字段数 y是输出格式 z是要显示的字段的长度
c用于字符 b用于字节
d用于十进制 h用于十六位字(半字)
x用于十六进制 w用于32位字

9. 在汇编语言中使用C库函数
* 开头使用的output里使用的是.aasciz命令,而不是.ascii。printf函数要求以空字符结尾的字符串作为输出字符串。.asciz命令在定义的字符串末尾添加空字符。

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

  • 使用.lcomm命令,使用以下参数是将包含厂商ID字符串的缓冲区。因为不需要定义缓冲区的值,所以在bss段中使用.lcomm命令把它声明为12字节的缓冲区区域:
.section .bss
.lcomm buffer, 12
  • 把参数传递给C函数printf,必须把他们压入堆栈。这是使用PUSHL指令完成。参数放入堆栈的顺序和printf函数获取他们的顺序是相反的,所以缓冲区值被首先放入,然后是输出字符串值,最后使用CALL指令调用printf函数,ADDL指令用于清空为printf函数放入堆栈的参数:

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

    1. 连接C库函数
  • 第一种方式为静态链接:静态链接把函数目标代码直接连接到应用程序的可执行程序文件中。会造成创建巨大的可执行程序、并且如果同时运行程序的多个实例,就会造成内存浪费(每个实例都有其自己的相同函数的拷贝)。
  • 第二种方式为动态连接:动态连接使用库的方式使程序员可以在应用程序中引用函数,但是不把函数代码连接到可执行程序文件。在程序运行时由操作系统调用动态链接库,并且多个程序可以共享动态链接库。
    Linux中标准C动态库位于libc.so.x文件中,使用-l参数时不用指定完整库名称

    ld -o cpuid -lc cpuid.o
  • 必须指定在运行时加载动态库的程序,在linux系统中,这个程序是ld-linux.so.2,通常在/lib下,为了指定这个程序,必须使用GNU连接器的-dynamic-linker参数

ld -dynamic-linker /lib/ld-linux.so.2 -o cpuid -lc cpuid.o
./cpuid