一, IA-32 硬件特性
寄存器:
1, 通用寄存器, 用于存放正在处理的数据
1 2 3 4 5 6 7 8 |
|
其中寄存器EAX, EBX, ECX, EDX又可以通过16位和8位寄存器名称引用 如EAX, AX 引用EAX低16位, AL 引用EAX低8位, AH 引用AL之后的高8位
2, 段寄存器:
IA-32平台允许使用3中内存模型: 平坦内存模式 分段内存模式 实地址模式
平坦内存: 把全部的系统内存表示为连续的地址空间, 通过线性地址的特定地址访问内存位置.
分段内存: 把系统内存划分为独立的段组, 通过位于寄存器中的指针进行引用. 每个段用于包含特定类型的数据。 一个段用于包含指令码, 另一个段包含数据元素, 第三个段包含数据堆栈。段中的内存位置是通过逻辑地址引用的, 逻辑地址是由段地址加上偏移量构成, 处理器把逻辑地址转换为相应的线性地址以便访问。
1 2 3 4 5 6 7 |
|
每个段寄存器都是16位的, 包含指向内存特定段起始位置的指针,程序不能显示加载或改变CS寄存器, DS, ES, FS, GS都用于指向数据段, 通过4个独立的段, 程序可以分隔数据元素, 确保他们不会重叠, 程序必须加载带有段的正确指针值的数据段寄存器, 并且使用偏移值引用各个内存的位置。SS段寄存器用于指向堆栈段, 堆栈包含传递给函数和过程的数据值。
实地址: 如果实地址模式, 所有段寄存器都指向线性0地址, 并且都不会被程序改动,所有的指令码 数据元素 堆栈元素 都是通过他们的线性地址直接访问的。
3, 指令指针寄存器
是EIP寄存器, 它跟踪要执行程序的下一条指令代码, 应用程序不能修改指令指针本身,不能指定内存地址把它拖放EIP寄存器中,相反必须通过一般的跳转指令来改变预存取缓存的下一条指令。
在平坦内存模型中, 指令指针包含下一条指令码的线性地址, 在分段模型中指令指针包含逻辑地址指针, 通过CS寄存器的内存引用。
4, 控制寄存器
1 2 3 4 5 |
|
不能直接访问控制寄存器, 但是能把控制寄存器中的值传递给通用寄存器,如果必须改动控制寄存器的标志, 可以改动通用寄存器的值, 然后把内容传递给控制寄存器。
标志:
IA-32使用单一的寄存器来包含一组状态控制和系统标志, EFLAGS寄存器包含32位标志信息
1, 状态标志
1 2 3 4 5 6 7 8 |
|
2, 控制标志
当前只定义了一个控制标志DF即方向标志, 用于控制处理器处理字符串的方式 如果设置为1, 字符串指令自动递减内存地址以便到达字符串中的下一字节。 反之。
3, 系统标志
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
二,GNU汇编工具系列
1, 二进制工具系列
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
2, GNU编译器
gcc
3, GNU调试程序
gdb
三, GNU汇编语言结构
主要包括三个常用的段:
data 数据段 声明带有初始值的元素
bss 数据段 声明使用0或者null初始化的元素
text 正文段 包含的指令, 每个汇编程序都必须包含此段
使用.section 指令定义段, 如:
1 2 3 |
|
起始点:
gnu汇编器使用start标签表示默认的起始点, 此外如果想要汇编内部的标签能够被外部程序访问, 需要使用.globl 指令, 如:.globl start
使用通用库函数时可以使用:
ld -dynamic-linker /lib/ld-linux.so.2
四, 数据传递
1, 数据段
使用.data声明数据段, 这个段中声明的任何数据元素都保留在内存中并可以被汇编程序的指令读取, 此外还可以使用.rodata声明只读的数据段, 在声明一个数据元素时, 需要使用标签和命令:
标签:用做引用数据元素所使用的标记, 它和c语言的变量很相似, 它对于处理器是没有意义的, 它 只是用做汇编器试图访问内存位置时用做引用指针的一个位置。
指令:这个名字指示汇编器为通过标签引用的数据元素保留特定数量的内存, 声明命令之后必须给出 一个或多个默认值。
声明指令:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
|
定义静态符号:
使用.equ命令把常量值定义为可以在文本段中使用的符号,如:
1 2 3 4 |
|
2, bss段
和data段不同, 无需声明特定的数据类型, 只需声明为所需目的保留的原始内存部分即可。 GNU汇编器使用以下两个命令声明内存区域:
1 2 |
|
两种声明很相似, 但.lcomm是为不会从本地汇编代码之外进行访问的数据保留的, 格式为:
1
|
|
例子:
1 2 |
|
该语句把1000字节的内存地址赋予标签buffer, 在声明本地通用内存区域的程序之外的函数是 不能访问他们的.(不能在.globl命令中使用他们)
在bss段声明的好处是, 数据不包含在可执行文件中。在数据段中定义数据时, 它必须被包含在 可执行程序中, 因为必须使用特定值初始化它。 因为不使用数据初始化bss段中声明的数据区域, 所以内存区域被保留在运行时使用, 并且不必包含在最终的程序中
3, 传送数据
move 指令: 格式 movex 源操作数, 目的操作数。 其中x为要传送数据的长度, 取值有:
l 用于32位的长字节
w 用于16位的字
b 用于8位的字节值
立即数前面要加一个$符号, 寄存器前面要加%符号。
8个通用的寄存器是用于保存数据的最常用的寄存器, 这些寄存器的内容可以传递
给其他的任何可用的寄存器。 和通用寄存器不同, 专用寄存器(控制, 调试, 段)
的内容只能传送给通用寄存器, 或者接收从通用寄存器传过来的内容。
在对标签进行引用时: 例:
1 2 3 4 5 6 7 8 |
|
其中:
movl value, %eax 只是把标签value当前引用的内存值传递给eax
movl $value, %eax 把标签value当前引用的内存地址指针传递给eax
movl %ebx, (%edi) 如果edi外面没有括号那么这个指令只是把ebx中的
值加载到edi中, 如果有了括号就表示把ebx中的内容
传送给edi中包含的内存位置。
movl %ebx, 4(%edi) 表示把edi中的值放在edi指向的位置之后的4字节内存位置中
movl %ebx, -4(%edi) 表示把edi中的值放在edi指向的位置之前的4字节内存位置中
cmove 指令(条件转移): cmovex 源操作数, 目的操作数. x的取值为:
无符号数:
1 2 3 4 5 6 7 8 9 10 |
|
有符号数:
1 2 3 4 5 6 7 |
|
交换数据: xchg 在两个寄存器之间或者寄存器和内存间交换值。如:
xchg 操作数, 操作数, 要求两个操作数必须长度相同且不能同时都是内存位置
其中寄存器可以是32,16,8位的
bswap 反转一个32位寄存器的字节顺序
如: bswap %ebx
xadd 交换两个值 并把两个值只和存储在目标操作数中
如: xadd 源操作数,目标操作数
其中源操作数必须是寄存器, 目标操作数可以是内存位置也可以是寄存器
其中寄存器可以是32,16,8位的
cmpxchg
cmpxchg source, destination
其中source必须是寄存器, destination可以是内存或者寄存器, 用来比较
两者的值, 如果相等,就把源操作数的值加载到目标操作数中, 如果不等就把
目标操作数加载到源操作数中,其中寄存器可以是32,16,8位的, 其中源操作
数是EAX,AX或者AL寄存器中的值
cmpxchg8b 同cmpxchg, 但是它处理8字节值, 同时它只有一个操作数
cmpxchg8b destination
其中destination引用一个内存位置, 其中的8字节值会与EDX和EAX寄存器中
包含的值(EDX高位寄存器, EAX低位寄存器)进行比较, 如果目标值和EDX:EAX
对中的值相等, 就把EDX:EAX对中的64位值传递给内存位置, 如果不匹配就把
内存地址中的值加载到EDX:EAX对中
4, 堆栈
ESP 寄存器保存了当前堆栈的起始位置, 当一个数据压入栈时, 它就会自动递减,反之其自动递增
压入堆栈操作:
1 2 3 |
|
*出堆栈操作:
1
|
|
其中source必须是16或32位寄存器或者内存位置, 当pop最后一个元素时ESP值应该和以前的相等
5,压入和*出所有寄存器
1 2 3 4 |
|
6,数据地址对齐
gas 汇编器支持.align 命令, 它用于在特定的内存边界对准定义的数据元素, 在数据段中.align命令紧贴在数据定义的前面
五,控制流程
无条件跳转:
1, 跳转
jmp location 其中location为要跳转到的内存地址, 在汇编中为定义的标签
2,调用
调用指令分为两个部分:
1, 调用call address 跳转到指定位置
2, 返回指令ret, 它没有参数紧跟在call指令后面的位置
执行call指令时,它把EIP的值放到堆栈中, 然后修改EIP以指向被调用的函数地址, 当被调用函数完成后, 它从堆栈获取过去的EIP的值, 并把控制权返还给原始程序。
3,中断
由硬件设备生成中断。 程序生成软件中断 当一个程序产生中断调用时, 发出调用的程序暂停, 被调用的程序接替它运行, 指令指针被转移到被调用的函数地址, 当调用完成时使用中断返回指令可以返回调原始程序。
条件跳转:
条件跳转按照EFLAGS中的值来判断是否该跳转, 格式为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 |
|
条件跳转不支持分段内存模型下的远跳转, 如果在该模式下进行程序设计必须使用程序逻辑确定条件是否存在, 然后实现无条件跳转, 跳转前必须设置EFLAGS寄存器
比较:
cmp operend1, operend2
进位标志修改指令:
CLC 清空进位标志(设置为0)
CMC 对进位标志求反(把它改变为相反的值)
STC 设置进位标志(设置为1)
循环:
loop 循环直到ECX寄存器为0
loope/loopz 循环直到ecx寄存器为0 或者没有设置ZF标志
loopne/loopnz 循环直到ecx为0或者设置了ZF标志
指令格式为: loopxx address 注意循环指令只支持8位偏移地址
六,数字
IA-32平台中存储超过一字节的数都被存储为小尾数的形式但是把数字传递给寄存器时, 寄存器里面保存是按照大尾数的形式存储
把无符号数转换成位数更大的值时, 必须确保所有的高位部分都被设置为零
把有符号数转换成位数更大的数时:
intel 提供了movsx指令它允许扩展带符号数并保留符号, 它与movzx相似, 但是它假设要传送的字节是带符号数形式
浮点数:
fld 指令用于把浮点数字传送入和传送出FPU寄存器, 格式:
1
|
|
其中source可以为32 64或者80位整数值
IA-32使用FLD指令用于把存储在内存中的单精度和双精度浮点值FPU寄存器堆栈中, 为了区分这两种长度GNU汇编器使用FLDS加载单精度浮点数, FLDL加载双精度浮点数
类似FST用于获取FPU寄存器堆栈中顶部的值, 并且把这个值放到内存位置中, 对于单精度使用FSTS, 对于双精度使用FSTL
七,基本数学运算
1, 加法
ADD source, destination 把两个整数相加
其中source可以是立即数内存或者寄存器, destination可以是内存或者寄存器, 但是两者不能同时都是内存位置
ADC 和ADD相似进行加法运算, 但是它把前一个ADD指令的产生进位标志的值包含在其中, 在处理位数大于32(如64)位的整数时, 该指令非常有用
2, 减法
SUB source, destination 把两个整数相减
NEG 它生成值的补码
SBB 指令, 和加法操作一样, 可以使用进位情况帮助执行大的无符号数值的减法运算. SBB在多字节减法操作中利用
进位和溢出标志实现跨数据边界的的借位特性
3,递增和递减
dec destination 递减
inc destination 递增
其中dec和inc指令都不会影响进位标志, 所以递增或递减计数器的值都不会影响程序中涉及进位标志的其他任何运算
4, 乘法
mul source 进行无符号数相乘 它使用隐含的目标操作数, 目标位置总是使用eax的某种形式, 这取决与源操作数的长度, 因此根据源操作数的长度, 目标操作数必须放在AL, AX, EAX中。 此外由于乘法可能产生很大的值, 目标位置必须是源操作数的两倍位置, 源为 8时, 应该是16, 源为16时, 应该为32, 但是当源为16位时intel为了向下兼容, 目标操作数不是存放在eax中, 而 是分别存放在DX:AX中, 结果高位存储在DX中, 地位存储在AX中。对于32位的源, 目标操作数存储在EDX:EAX中, 其中 EDX存储的是高32位, EAX存储的是低32位
imul source 进行有符号数乘法运算, 其中的目标操作数和mul的一样
imul source, destination 也可以执行有符号乘法运算, 但是此时可以把目标放在指定的位置, 使用这种格式的缺陷 在与乘法的操作结果被限制为单一目标寄存器的长度.
imul multiplier, source, destination 其中multiplier是一个立即数, 这种方式允许一个值与给定的源操作数进行快速的乘法运算, 然后把结果存储在通用 寄存器中
5, 除法
div divisor 执行无符号数除法运算
除数的最大值取决与被除数的长度, 对于16位被除数 ,除数只能为8位, 32或64位同上
被除数 被除数长度 商 余数
AX 16位 AL AH
DX:AX 32位 AX DX
EDX:EAX 64位 EAX EDX
idiv divisor 执行有符号数的除法运算, 方式和div一样
6, 移位
左移位:
sal 向左移位
sal destination 把destination向左移动1位
sal %cl, destination 把destination的值向左移动CL寄存器中指定的位数
sal shifter, destination 把destination的值向左移动shifter值指定的位数
向左移位可以对带符号数和无符号数执行向左移位的操作, 移位造成的空位用零填充, 移位造成的超过数据长度的任何位
都被存放在进位标志中, 然后在下一次移位操作中被丢弃
右移位:
shr向右移位
sar向右移位
SHR指令清空移位造成的空位, 所以它只能对无符号数进行移位操作
SAR指令根据整数的符号位, 要么清空, 要么设置移位造成的空位, 对于负数, 空位被设置为1
循环移位:
和移位指令类似, 只不过溢出的位被存放回值的另一端, 而不是丢弃
ROL 向左循环移位
ROR 向右循环移位
RCL 向左循环移位, 并且包含进位标志
RCR 向右循环移位, 并且包含进位标志
7, 逻辑运算
AND OR XOR
这些指令使用相同的格式:
and source, destination
其中source可以是8位 16 位或者32位的立即值 寄存器或内存中的值, destination可以是8位 16 位或者
32位寄存器或内存中的值, 不能同时使用内存值作为源和目标。 布尔逻辑功能对源和目标执行按位操作。
也就是说使用指定的逻辑功能按照顺序对数据的元素的每个位进行单独比较。
NOT指令使用单一操作数, 它即是源值也是目标结果的位置 清空寄存器的最高效方式是使用OR指令对寄存器和它本身进行异或操作.当和本身进行XOR操作时, 每个设置为 1的位就变为0, 每个设置为0的位也变位0。
位测试可以使用以上的逻辑运算指令, 但这些指令会修改destination的值, 因此intel提供了test指令, 它不 会修改目标值而是设置相应的标志
八,字符串处理
1, 传送字符串
1 2 3 4 |
|
movs指令使用隐含的源和目的操作数, 隐含的源操作数是ESI, 隐含的目的操作数是EDI, 有两种方式加载内存地址到
ESI和EDI, 第一种是使用标签间接寻址 movl $output, %ESI, 第二种是使用lea指令, lea指令加载对象的地址到指定
的目的操作数如lea output, %esi, 每次执行movs指令后, 数据传送后ESI和EDI寄存器会自动改变,为另一次传送做
准备, ESI和EDI可能随着标志DF的不同自动递增或者自动递减, 如果DF标志为0则movs指令后ESI和EDI会递增, 反之会
递减, 为了设置DF标志, 可以使用一下指令:
CLD 将DF标志清零
STD 设置DF标志
2,rep前缀
REP 指令的特殊之处在与它不执行什么操作, 这条指令用于按照特定次数重复执行字符串指令, 有ECX寄存器控制,
但不需要额外的loop指令, 如rep movsl
rep的其他格式:
repe 等于时重复
repne 不等于时重复
repnz 不为零时重复
repz 为零时重复
3, 存储和加载字符串
LODS 加载字符串, ESI为源, 当一次执行完lods时会递增或递减ESI寄存器, 然后把字符串值存放到EAX中
STOS 使用lods把字符串值加载到EAX后, 可以使用它把EAX中的值存储到内存中去: stos使用EDI作为目的操作数, 执行stos指令后, 会根据DF的值自动递增或者递减EDI中的值
4, 比较字符串
cmps 和其他的操作字符串的指令一样, 隐含的源和目标操作数都为ESI和EDI, 每次执行时都会根据DF的值把 ESI和EDI递增或者递减, cmps指令从目标字符串中减去源字符串, 执行后会设置EFLAGS寄存器的状态.
5,扫描字符串
scas 把EDI作为目标, 它把EDI中的字符串和EAX中的字符串进行比较 ,然后根据DF的值递增或者递减EDI
九,使用函数
GNU汇编语言定义函数的语法:
.type 标签(也就是函数名), @function
ret 返回到调用处
十,linux系统调用
linux系统调用的中断向量为0x80
1, 系统调用标识存放在%eax中
2, 系统调用输入值:
1 2 3 4 5 |
|
需要输入超过6个输入参数的系统调用, EBX指针用于保存指向输入参数内存位置的指针, 输入参数按照连续的的顺序 存储, 系统调用的返回值存放在EAX中
十一,汇编语言的高级功能
1,gnu内联汇编的语法:
asm
或__asm__
(“汇编代码”);
指令必须包含在引号里
如果包含的指令超过一行 必须使用新行分隔符分隔
使用c全局变量, 不能在内联汇编中使用局部变量, 注意在汇编语言代码中值被用做内存位置, 而不是立即数值
如果不希望优化内联汇编, 则可以volatile修饰符如:__asm__ volatile
("code");
2,GCC内联汇编的扩展语法
__asm__("assembly code":output locations:input operands:changed registers);
第一部分是汇编代码
第二部分是输出位置, 包含内联汇编代码的输出值的寄存器和内存位置列表
第三部分是输入操作数,包含内联汇编代码输入值的寄存器和内存位置的列表
第四部分是改动的寄存器, 内联汇编改变的任何其他寄存器的列表
这几个部分可以不全有, 但是没有的还必须使用:分隔
1, 指定输入值和输出值, 输入值和输出值的列表格式为:
“constraint”(variable), 其中variable是程序中声明的c变量, 在扩展asm格式中, 局部和全局变量都可以使用, 使用constrant(约束)定义把变量存放到哪(输入)或从哪里传送变量(输出) 约束使用单一的字符, 如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
|
除了这些约束之外, 输出值还包含一个约束修饰符:
1 2 3 4 5 |
|
如:
__asm__("assembly code": "=a"(result):"d"(data1),"c"(data2));
把c变量data1存放在edx寄存器中, 把c变量data2存放到ecx寄存器中, 内联汇编的结果
将存放在eax寄存器中, 然后传送给变量result
在扩展的asm语句块中如果要使用寄存器必须使用两个百分号符号
不一定总要在内联汇编代码中指定输出值, 一些汇编指令假定输入值包含输出值, 如movs指令
其他扩展内联汇编知识:
1, 使用占位符
输入值存放在内联汇编段中声明的特定寄存器中, 并且在汇编指令中专门使用这些寄存器. 虽然这种方式能够很好的处理只有几个输入值的情况, 但对于需要很多输入值的情况, 这 中方式显的有点繁琐. 为了帮助解决这个问题, 扩展asm格式提供了占位符, 可以在内联 汇编代码中使用它引用输入和输出值.
占位符是前面加上百分号的数字, 按照内联汇编中列出的每个输入和输出值在列表中的位置, 每个值被赋予从0开始的地方. 然后就可以在汇编代码中引用占位符来表示值。
如果内联汇编代码中的输入和输出值共享程序中相同的c变量, 则可以指定使用占位符作为 约束值, 如:
1 2 3 |
|
如输入输出值**享相同的变量data2, 而在输入变量中则可以使用标记0作为输入参数的约束
2, 替换占位符
如果处理很多输入和输出值, 数字型的占位符很快就会变的很混乱, 为了使条理清晰 ,GNU汇编 器(从版本3.1开始)允许声明替换的名称作为占位符.替换的名称在声明输入值和输出值的段中 定义, 格式如下:
1 2 3 4 5 |
|
3, 改动寄存器列表
编译器假设输入值和输出值使用的寄存器会被改动, 并且相应的作出处理。程序员不需要在改动的 寄存器列表中包含这些值, 如果这样做了, 就会产生错误消息. 注意改动的寄存器列表中的寄存器 使用完整的寄存器名称, 而不像输入和输出寄存器定义的那样仅仅是单一字母。 在寄存器名称前面 使用百分号符号是可选的。
改动寄存器列表的正确使用方法是, 如果内联汇编代码使用了没有被初始化地声明为输入或者输出 值的其他任何寄存器 , 则要通知编译器。编译器必须知道这些寄存器, 以避免使用他们。如:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
4, 使用内存位置
虽然在内联汇编代码中使用寄存器比较快, 但是也可以直接使用c变量的内存位置。 约束m用于引用输入值 和输出值中的内存位置。 记住, 对于要求使用寄存器的汇编指令, 仍然必须使用寄存器, 所以不得不定义 保存数据的中间寄存器。如:
1 2 3 4 5 6 7 8 9 10 11 12 |
|
5, 处理跳转
内联汇编语言代码也可以包含定义其中位置的标签。 可以实现一般的汇编条件分支和无条件分支, 如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
在内联汇编代码中使用标签时有两个限制。 第一个限制是只能跳转到相同的asm段内的标签, 不能从-个asm段跳转到另一个asm段中的标签。第二个限制更加复杂一点。 以上程序使用 标签greater和end。 但是, 这样有个潜在的问题, 查看汇编后的代码清单, 可以发现内联 汇编标签也被编码到了最终汇编后的代码中。 这意味着如果在c代码中还有另一个asm段, 就 不能再次使用相同的标签, 否则会因为标签重复使用而导致错误消息。还有如果试图整合使用 c关键字(比如函数名称或者全局变量)的标签也会导致错误。