汇编基础(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,注意计算机数据分布的大小端表示。要是这样写程序,会让人疯掉的,所以出现了下面要说的高级语言(编译型和解释型)。
例如指令码:C7 45 FC 10 00 00 00,它定义操作码C7,把值传送到内存位置的指令。内存位置由45 FC表示,45表示EBP寄存器中的值,FC表示所指示的内存地址开始的4个字节。10 00 00 00表示放置到这个内存块里的数值内容,其实就是1,注意计算机数据分布的大小端表示。要是这样写程序,会让人疯掉的,所以出现了下面要说的高级语言(编译型和解释型)。
高级语言到机器语言
总要有个高级语言到机器语言的转换过程,最终还是转换为CPU认识的指令语句,过程如下:
这个过程其实就两个步骤:
- 把高级语言(C/C++等)编译为目标代码。
- 连接原始指令码来生成可执行文件。
这里特别说明JAVA,其被编译为字节码的形式,字节码和处理器上看到的指令码类似,但它本身比不和任何系列的处理器兼容,相反,JAVA通过JAVA虚拟机(JVM)进行解释,JAVA虚拟机单独运行在宿主的计算机上。JAVA字节码是可以移植的,就是说它可以同过任何类型的宿主计算机的任何JVM来运行。其优势在与ORACLE提供了不同平台的特定JVM,这些JVM用于解释形同的字节码,从而无需从源代码重新编译。
汇编语言构成
使用助记符表示指令码,类似于使用英文样式的词语表示指令码,而让汇编器来讲汇编语言助记符转换为原始指令程序,汇编语言的构成:
1、操作码助记符,如常见的push、mov;助记符没有统一的标准,因不同的汇编器而不同,所以有上面讲的选择一个汇编的学习环境。
2、数据段,用来决定在内存的何处存储何种类型的数据。两种方式用来检索和存储数据:
(1)、使用内存定义,如
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 .dataoutput:
.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); */
-------------------------------------------------------------------------------------------------------------
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'
(.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
- 汇编基础(1)
- 汇编基础1
- 1 汇编基础
- 8086汇编1,零基础
- 汇编学习--汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 汇编基础
- 文件的读写和上锁
- ORACLE 11G ADG的STATSPACK性能报表配置方法
- IOS开发中用@interface声明局部变量ivar和用@property声明属性的区别
- codeforces 190D Non-Secret Cypher(two pointers)
- 进度条
- 汇编基础(1)
- POI 18
- android背景选择器selector用法
- 自适应的布局:使用输入法时底部Button被顶上来
- 整maven+ssm出现的错误 java.lang.UnsupportedClassVersionError: .... : Unsupported major.minor version 51.0
- 45个实用的JavaScript技巧、窍门和最佳实践
- 葡京娱乐场足球免费推荐交流群227231326
- 题目 E: 敲7(多实例测试)
- (每日算法)LeetCode --- Decode Ways