汇编基础(1)

来源:互联网 发布:和我信造粉软件 编辑:程序博客网 时间:2024/05/16 06:31

        一直想了解下汇编,觉得Richard Blum的《汇编语言程序设计》还是相对讲的通俗、实践性强一点。选择它主要还是因为和Linux打交道的日子比较多,书里的汇编版本也是比较经常会见到的AT&T的风格。毕竟它是一门与机器相关的语言,选择一个什么样的环境来了解还是需要考虑一下的,这里用Intel IA-32平台 + Ubuntu 10.04(2.6.32),文中的插图摘自原书。


机器指令码格式

注意,这里的指令码其实就是若干字节的十六进制数据。

一、修饰符
定义执行功能中涉及到的寄存器和内存位置,从图上看一共包含三部分
  • 寻址方式说明符字节(ModR/M)
  • 比例-索引-基址字节(SIB)
  • 1、2或4个地址位移字节
二、数据元素
表示静态数据(比如要加的数据)或者内存位置, 其长度可以包含1、2或者4个字节的信息。
例如指令码:C7 45 FC 10 00 00 00,它定义操作码C7,把值传送到内存位置的指令。内存位置由45 FC表示,45表示EBP寄存器中的值,FC表示所指示的内存地址开始的4个字节。10 00 00 00表示放置到这个内存块里的数值内容,其实就是1,注意计算机数据分布的大小端表示。要是这样写程序,会让人疯掉的,所以出现了下面要说的高级语言(编译型和解释型)。


高级语言到机器语言

总要有个高级语言到机器语言的转换过程,最终还是转换为CPU认识的指令语句,过程如下:

这个过程其实就两个步骤:
  • 把高级语言(C/C++等)编译为目标代码。
  • 连接原始指令码来生成可执行文件。
目标代码(*.obj)在宿主机上不能运行的,因为每种操作系统只能识别特定的可执行文件的格式,而且当前的目标代码很有可能要和其他的目标代码的功能,于是连接器(Linker)就产生了。连接器把应用程序的目标代码文件和应用程序所有需要的附加其他的目标文件连接起来,并且生成目标操纵系统(这里考虑交叉编译的情况)可以识别的可执行文件输出格式,这个过程还是比较容易理解的。
这里特别说明JAVA,其被编译为字节码的形式,字节码和处理器上看到的指令码类似,但它本身比不和任何系列的处理器兼容,相反,JAVA通过JAVA虚拟机(JVM)进行解释,JAVA虚拟机单独运行在宿主的计算机上。JAVA字节码是可以移植的,就是说它可以同过任何类型的宿主计算机的任何JVM来运行。其优势在与ORACLE提供了不同平台的特定JVM,这些JVM用于解释形同的字节码,从而无需从源代码重新编译。


汇编语言构成

使用助记符表示指令码,类似于使用英文样式的词语表示指令码,而让汇编器来讲汇编语言助记符转换为原始指令程序,汇编语言的构成:

1、操作码助记符,如常见的push、mov;助记符没有统一的标准,因不同的汇编器而不同,所以有上面讲的选择一个汇编的学习环境。
2、数据段,用来决定在内存的何处存储何种类型的数据。两种方式用来检索和存储数据:
   (1)、使用内存定义,如
        testvalue:
             .long 150
        message:
             .ascii "Hello world"
        pi:
          .float 3.14159
   (2)、使用堆栈(stack)
     使用堆栈指针进出栈来传递数据,在函数调用时,常常把希望传递给函数的参数放到堆栈的顶端,函数被调用时可以从堆栈查找元素。                        
3、命令,不同的汇编器所使用的命令有区别,其中最为重要的命令之一是.section命令,它定义内存段,汇编语言程序在其中定义元素,所有的汇编语言程序至少有三个必须声明的段落:
  • 数据段,存储数据元素的内存区域,该段不能扩展,在整个程序中保持静态。
  • BSS端,也是静态内存段,包含用于以后再程序中声明的数据的缓冲区。其内存区域是由0填充。
  • 文本段,是内存中存储指令的区域,同样,这一区域也是固定的,其中之包含汇编语言程序中声明的指令码。

处理器寄存器

IA-32系列所有处理器都可以使用的寄存器的核心组如下:

1、通用寄存器
处理器处理数据时,通用寄存器用于临时的存储数据,完全向下兼容,一些通用寄存器列表如下:
  • EAX 用于操作数和结果数据的累加器
  • EBX 指向数据内存段的数据指针,即存着数据段的某个内存的地址。
  • ECX 字符串和循环操作的计数器
  • EDX I/O指针
  • EDI 用于字符串操作的目标数据指针,即目标字符串的指针。
  • ESI 用于字符串操作的源数据指针,即源字符串的指针
  • ESP 堆栈指针
  • EBP 基址指针 /* ESP始终指向栈顶,EBP是在堆栈中寻址用的 */
2、段寄存器
IA-32允许3中不同的内存访问方式:
  • 平坦式,把全部的系统内存表示为连续的系统内存空间,同步线性地址的特定地址访问每个内存位置。l
  • 分段内存,把系统内存划分为独立段组,同步位于段寄存器中的指针进行引用,每个端用于包含特定内存的数据,如不同的段分表表示,堆栈,指令,数据。段内内存地址是通过逻辑地址定义的,逻辑地址有段地址和段内偏移地址构成,处理器把逻辑地址转换为相应的线性地址位置访问内存。段寄存器(16位)用于包含特定数据访问的段地址:

DS ES FS GS都是表示指向数据段的,程序因此可以分隔数据元素,确保其不重叠。
SS寄存器指向堆栈段,包含传递给程序中的函数和过程的数据值。
  • 实地址模式,所有的段寄存器都指向零线性地址,并且都不会被程序改动,所有的指令、数据、堆栈都是通过线性地址访问。
3、指令指针寄存器(程序计数器)---EIP,跟踪要执行的下一条指令码,必须使用一般的程序控制,如jmp来改变要预取得下一条指令,分段模式下,使用CS寄存器的引用。

4、控制寄存器,确定处理器的执行模式,还有当前任务的特征:

不能直接访问控制寄存器的值,但可以把控制寄存器的值传送给通用寄存器,便可以查看其内容。同样如需要改动控制寄存器的值,也是通过通用寄存器改动后传送回去,系统程序员干的活。
5、标志寄存器----EFLAGS寄存器(32位),处理器的操作是否成功,用标记来实现这个功能。
  • 状态标志

  • 控制标志
仅定义了一个控制标志---DF标志,方向标志用于控制住处理器处理字符串的方式,置1时表示递减内存地址到达下一个字符,否则递增内存地址到达下一个字符的字节。
  • 系统标志



软件工具

binutils包

外加gcc/g++和gdb


汇编程序范例

1、定义起点
汇编语言被转换为可执行文件时,链接器必须知道代码的起点是什么,GNU汇编声明了一个默认的标签,或者说标识符,作为当前应用程序的的入口点。_start标签用于程序应该从这条指令开始运行,连接器也会去寻找这个标签。但是我们编写的程序是由操作系统的外部程序调用的,还应该再提供一个外部应用程序的入口点,这是使用.globl命令完成的。.globl声明外部程序的标签。如果编写被外部汇编语言或者C语言程序使用的一组工具,就应该使用.globl命令声明每个函数段标签。

2、GNU使用.section命令语句声明段。.section语句只使用一个参数----它声明段的类型:
.section .data
     <initialized data here>

.section .bss
     <uninitialized data here>

.section .text
.globl _start
_start:
     <instrument code goes here>

3、简单的汇编程序
CPUID指令使用单一寄存器值进行输入。EAX寄存器用于决定CPUID的输出信息,根据EAX寄存器的值,该指令在EBX、ECX、EDX寄存器中生成关于处理器的不同信息,信息根据一系列的位置和标志返回,必须解释出它们的正确含义。

当EAX寄存器为零时,CPUID指令返回简单的厂商ID字符串,字符串被按顺序返回到寄存器EBX、EDX、ECX中,
  • EBX包含字符串的最低4个字节。
  • EDX包含字符串的中间4个字节。
  • ECX包含字符串的最高4个字节
以小段格式存储的话,刚开始的字符串应该输出到EBX寄存器中,源程序如下:
.section .data
output:
    .ascii "The processer Vendor ID is 'xxxxxxxxxxxx'\n" /* ascii 字符 */

.section .text
.globl _start
_start:
    movl $0, %eax
    cpuid

    movl $output, %edi      /* 字符串操作的目的数据指针 */
    movl %ebx, 28(%edi)     /* 覆盖output内的特定字节 */
    movl %edx, 32(%edi)
    movl %ecx, 36(%edi)

    movl $4, %eax           /* 表示使用内核的系统调用的代号, write(4), write(fd, buf, len) */
    movl $1, %ebx           /* write所操作的描述符 */
    movl $output, %ecx      /* write所操作的字符串的开始 */
    movl $42, %edx          /* write的第三个参数, 写入数据的长度 */
    int  $0x80              /* 调用系统调用, 从内核访问控制台显示, linux提供了很多可以从汇编程序访问的预置函数, 必须使用该语句生成软中断 */

    movl $1, %eax           /* 表示使用内核的exit(1)系统调用 */
    movl $0, %ebx           /* exit系统调用的参数 */
    int  $0x80              /* 调用exit(0) */
-----------------------------------------------------------------------------------------
Intel机器上运行:

AMD机器上运行:


使用GNU高级语言的编译器(gcc)编译,因为GNU链接器查找_start确定程序的开始位置,gcc查找的是main标签确认开始位置,使用前需要将_start标签替换为main即可。
$ g++ -o cpuid cpuid.s -Wall


调试汇编程序(gdb)

1、增加调试信息
$ as -g -o cpuid.o cpuid.s; 
$ ld -o cpuid cpuid.o

2、调试方法
$ b _start //在_start标签设置断点
$ i r      //查看所有的寄存器的值
$ print    //显示特定的寄存器或特定变量的值,print/d显示十进制,print/t显示二进制,print/x显示十六进制
如print/x $ebx
x          //显示特定内存位置的内容,x/nyz,其中n表示显示字段(z)总数,y表示输出格式(c表示字符,d表示十进制,x表示十六进制),z表示要显示的字段的长度(b表示字节,h表示16位字,w表示32位字),例如,x/42cb &output,以字符方式显示output变量地址处开始的42个字节内容,即打印output字符串而已。



汇编语言中使用C库函数

使用C库函数printf打印处理器的vendor,cpuid_c.s如下:
.section .data
output:
    .asciz "The processer Vendor ID is %s\n" /* ascii 字符串,注意这里不是.ascii */

.section .bss
    .lcomm buffer, 12        /* 声明12个字节的缓冲区 */

.section .text
.globl _start
_start:
    movl $0, %eax
    cpuid

    movl $buffer, %edi      /* 字符串操作的目的数据指针 */
    movl %ebx, 0(%edi)      /* 覆盖output内的特定字节 */
    movl %edx, 4(%edi)
    movl %ecx, 8(%edi)

    pushl $buffer          /* 后输出, 先入栈, 作为printf函数的参数 */
    pushl $output          /* 先输出, 后入栈, 作为printf函数的参数 */
    call printf            /* printf(output, buffer); */

    addl $8, %esp          /* 将栈指针向下移动8个字节(因上面pushl了两参数), 情况了printf放入了堆栈的参数 */
    pushl $0               /* 传入0作为函数调用的参数 */
    call exit              /* exit(0); */
-------------------------------------------------------------------------------------------------------------
$ as -o cpuid_c.o cpuid_c.s
$ ld -o cpuid_c cpuid_c.o
因为没有连接C的库文件,连接过程会报错:
cpuid_c.o: In function `_start':
(.text+0x1f): undefined reference to `printf'
cpuid_c.o: In function `_start':
(.text+0x29): undefined reference to `exit'
这里连接C的动态库libc.so.*
$ ld -lc -o cpuid_c cpuid_c.o
出现了一个有趣的问题,该语句生成了cpuid_c的可执行文件,但是用./cpuid_c执行文件时缺出现:-bash: ./cpuid_c: No such file or directory
问题是连接器能够解析C函数,但函数本身并没有在可执行文件中,因为使用的是动态连接,解决这个问题还必须在运行时指定要加载的动态库程序,这个程序是ld-linux.so.2,通常在/lib下,为了这个程序,必须使用GNU链接器的-dynamic-linker参数:
$ ld -dynamic-linker /lib/ld-linux.so.2 -lc -o cpuid_c cpuid_c.o
这样执行就没有问题了:

也可以使用gcc直接编译,它会自动连接必须的C库,无需进行任何特殊的操作,需要注意的是,用gcc编译时要将_start标签改为main,之后就是简单的编译了:
gcc -o cpuid_c cpuid_c.s -Wall
./cpuid_c
GNU编译器能自动连接正确的C函数。
<完>.

0 0