3.1程序编码
实际的汇编语言有多个版本根据处理器的指令集来决定有哪些命令这里讲述的是其中一种比较简单的汇编语言,汇编语言现在的应用有逆向工程和一些嵌入式硬件的编程。
3.1.1机器级代码
x86-64的机器代码和原始的C代码差别非常大。一些通常对C语言程序员隐藏的处理器状态是可见的:
- 程序计数器:给出将要执行的下一条指令在内存中的地址。
- 整数寄存器文件:包含16个命名的位置,分别存储64位的值。这些寄存器可以存储地址或整数数据。有的寄存器被用来记录某些重要的程序状态,而其它寄存器用来保存临时数据(参数,返回值,局部变量)。
- 条件码寄存器:保存着最近执行的算术或逻辑指令的状态信息。它们用来实现控制或数据流中的条件变化,比如用来实现if和while语句。
- 一组寄存器可以存放一个或多个整数或浮点数的值。
C语言的数据类型对于汇编语言来说只是一组连续的字节数,C语言中的数组和结构体在汇编语言上是一个很大的,按字节寻址的数据类型。
3.1.2代码示例
C语言代码:
long mult2(long,long);
void mulstore(long x,long y,long *data){
long t=mult2(x,y);
*dest=t;
}
对应的汇编代码:
multstore:
pushq %rbx
movq %rdx, %rbx
call mult2
movq %rax, (%rbx)
popq %rbx
ret
上面发代码每个缩进都对应于一条机器指令。比如pushq指令表示应该将寄存器%的内容压入程序栈。
3.2数据格式
C声明和本书所讲的汇编的数据类型对应如下表:
C声明 | 汇编数据类型 | 汇编代码后缀 | 大小(字节) |
char | 字节 | b | 1 |
short | 字 | w | 2 |
int | 双字 | l | 4 |
long | 四字 | q | 8 |
char* | 四字 | q | 8 |
float | 单精度 | s | 4 |
double | 双精度 | l | 8 |
注意:表中的l既是双字也是双精度是因为整型数据和浮点数所使用的指令和寄存器不同,图中的各数据类型都是32位的。
3.3访问信息
一个x86-64的CPU包含一组16个存储64位值的通用目的寄存器。这些寄存器用来存储整数数据和指针。下图显示了这16个寄存器。它们名字都以%r开头。规律可以自己看出
63 | 31 | 15 | 7 | 0 | |
%rax | %eax | %ax | %al | 返回值 | |
%rbx | %ebx | %bx | %bl | 被调用者保存 | |
%rcx | %ecx | %cx | %cl | 第四个参数 | |
%rdx | %edx | %dx | %dl | 第三个参数 | |
%rsi | %esi | %si | %sil | 第二个参数 | |
%rdi | %edi | %di | %dil | 第三个参数 | |
%rbp | %ebp | %bp | %bpl | 被调用者保存 | |
%rsp | %esp | %sp | %spl | 栈指针 | |
%r8 | %r8d | %r8w | %r8b | 第五个参数 | |
%r9 | %r9d | %r9w | %r9b | 第六个参数 | |
%r10 | %r10d | %r10w | %r10b | 调用者保存 | |
%r11 | %r11d | %r11w | %r11b | 调用者保存 | |
%r12 | %r12d | %r12w | %r12b | 被调用者保存 | |
%r13 | %r13d | %r13w | %r13b | 被调用者保存 | |
%r14 | %r14d | %r14w | %r14b | 被调用者保存 | |
%r15 | %r15d | %r15w | %r15b | 被调用者保存 |
表头的第一行表示操作的位数(注意是从0算其),最后一列表示寄存器的作用。
3.3.1操作数指示符
大多数指令有一个或多个操作数,指示出执行一个操作要使用的源数据值,以及放置结果的目的位置。x86-64支持多种操作数格式,包括三种,第一种是立即数,用来表示常数值,第二种是寄存器,它表示某个寄存器的内容。第三种是内存引用,它会根据计算出来的地址访问某个内存位置。操作数格式如下图;
类型 | 格式 | 操作数值 | 名称 |
立即数 | $Imm | Imm | 立即数寻址 |
寄存器 | r1 | R[r1] | 寄存器寻址 |
存储器 | Imm | M[Imm] | 绝对寻址 |
存储器 | (r1) | M[R[r1]] | 间接寻址 |
存储器 | Imm(r2) | M[Imm+R[r2]] | (基址+偏移量)寻址 |
存储器 | (r1,r2) | M[R[r1]+R[r2]] | 变址寻址 |
存储器 | Imm(r1,r2) | M[Imm+R[r1]+R[r2]] | 变址寻址 |
存储器 | (,r1,s) | M[R[r1]*s] | 比例变址寻址 |
存储器 | Imm(,r1,s) | M[Imm+R[r1]*s] | 比例变址寻址 |
存储器 | (r1,r2,s) | M[R[r1]+R[r2]*s] | 比例变址寻址 |
存储器 | Imm(r1,r2,s) | M[Imm+R[r1]+R[r2]*s] | 比例变址寻址 |
如下是个示例:
地址 | 值 |
0x100 | 0xFF |
0x104 | 0xAB |
0x108 | 0x13 |
0x10C | 0x11 |
寄存器 | 值 |
%rax | 0x100 |
%rcx | 0x1 |
%rdx | 0x3 |
这是书上的一道题这里就作为一个示例:
操作数 | 值 | 注释 |
%rax | 0x100 | 寄存器 |
0x104 | 0xAB | 绝对地址 |
$0x108 | 0x108 | 立即数 |
(%rax) | 0xFF | 地址0x100 |
4(%rax) | 0xAB | 地址0x104 |
9(%rax,%rdx) | 0x11 | 地址0x10C |
260(%rcx,%rdx) | 0x13 | 地址0x108 |
0xFC(,%rcx,4) | 0xFF | 地址0x100 |
(%rax,%rdx,4) | 0x11 | 地址0x10C |
3.3.2数据传送指令
最频繁使用的指令是将一个数据从一个位置复制到另一个位置的指令。数据传送指令就是用来实现赋值操作的具体命令如下表
指令 | 效果 | 描述 |
Mov S,D | D<-S | 传送 |
movb | 传送字节 | |
movw | 传送字 | |
movl | 传送双字 | |
movq | 传送四字 | |
movabsq I,R | R<-I | 传送绝对的四字 |
这些mov指令都是由mov+数据类型后缀组成,mov指令其第一个操作数即S是其传送位置的值可以是一个立即数,存储在寄存器或内存中。第二个是目的操作数是其要赋值的位置可以是一个内存地址或是一个寄存器。
下面给出一些示例:
movl $0x4050,%eax
movw %bp,%sp
movb (%rdi,%rcx),%al
movb %-17,(%rsp)
movq %rax,-12(%rbp)
第一行传送的是双字,传送的值是个立即数,目的地是寄存器
第二行传送的是字,传送的是寄存器的值,目的的是寄存器
第三行传送的是字节,传送的是对应内存地址的值,目的地是寄存器
第四行传送的是字节,传送的是个立即数,目的地是寄存器的值的对应内存地址
第五行传送的是四字,传送的是寄存器的值,目的地是内存地址
除了上述的传送指令还有零扩展和符号扩展的传送指令
指令 | 效果 | 描述 |
MOVZ S,R | R<-零扩展(S) | 以零扩展进行传送 |
movzbw | 将做了零扩展的字节传送到字 | |
movzbl | 将做了零扩展的字节传送到双字 | |
movzwl | 将做了零扩展的字传送到双字 | |
movzbq | 将做了零扩展的字节传送到四字 | |
movzwq | 将做了零扩展的字传送到四字 |
符号扩展mov指令
指令 | 效果 | 描述 |
MOVS S,R | R<-符号扩展(S) | 传送符号扩展的字节 |
movsbw | 将做了符号扩展的字节传送到字 | |
movsbl | 将做了符号扩展的字节传送到双字 | |
movswl | 将做了符号扩展的字传送到双字 | |
movsbp | 将做了符号扩展的字节传送到四字 | |
movswp | 将做了符号扩展的字传送到四字 | |
movslq | 将做了符号扩展的双字传送到四字 | |
cltq | %rax<-符号扩展(%eax) | 把%eax符号扩展到%rax |
数据传送示例:
C语言代码:
long exchange(long *xp , long y)
{
long x = *xp;
*xp = y;
return x;
}
汇编代码:
exchange:
movq (%rdi),%rax
movq %rsi, (%rdi)
ret
其中xp在rdi,y在rsi
3.3.3压入和弹出栈数据
首先先说一下什么是栈
所谓栈就是内存上用来存放程序函数数据的内存区域即用来存放函数的局部变量和函数的代码段的一块内存地址。
所有的程序都是从man函数开始的才程序运行时首先会把man函数压入栈当调用其它函数时也会把调用的函数压入栈当调用完毕后就将该函数弹出栈在高级语言的编写中不需要考虑,但是汇编语言还是需要学习一下的其命令如下。
指令 | 效果 | 描述 |
pushq S | R[%rsp]<-R[%rsp]-8; M[R[%rsp]<-S; | 将四字压入栈 |
popq D | D<-M[R[%rsp]; R[%rsp]<-R[%rsp]+8; | 将四字弹出栈 |
pushq %rbp的行为等价于:
subq $8 %rsp
movq %rbp,(%rsp)
下图是一个栈操作的说明图:
popq %rdx的行为等价于:
movq (%rsp),%rax
addq $8,%rsp
3.4算术和逻辑操作
下表列出了汇编语言的各种算术和逻辑运算的指令:
指令 | 效果 | 描述 |
leaq S,D | D<-&S | 加载有效地址 |
INC D DEC D NEG D NOT D | D<-D+1 D<-D-1 D<- -D D<- ~D | 加1 减1 取负 取补 |
ADD S,D SUB S,D IMUL S,D XOR S,D OR S,D AND S,D | D<-D+S D<-D-S D<-D*S D<-D^S D<-D|S D<-D&S | 加 减 乘 异或 或 与 |
SAL k,D SHL k,D SAR k,D SHR k,D | D<-D<<k D<-D<<k D<-D>>k D<-D>>k | 左移 左移(等同于SAL) 算术右移 逻辑右移 |
3.4.1加载有效地址
指令leap实际上是movq指令的变体,它的指令形式是从内存读数据到寄存器,但实际它根本没有引用内存。
3.5控制
3.5.1条件码
条件码可以看作是C语言的一些逻辑判断之后的结果不过这里有了分类在C语言里只有两种结果,而在汇编则是分为各种不同的寄存器来负责存放结果。
除了整数寄存器,CPU还维护着一组单个位的条件码寄存器,它们描述了最近的算术或逻辑操作的属性。可以检测这些寄存器来执行分支指令。最常用的条件码有:
CF:进位标志。最近操作使最高位产生了进位。可用来检测无符号的溢出。
ZF:零标志。最近的操作得出的结果为0。
SF:符号标志。最近的操作结果为负数。
OF:溢出标志。最近的操作导致一个补码溢出——正溢出或负溢出。
除了逻辑操作指令外还有如下指令(这些指令只负责设置条件码不会改变任何寄存器或内存,就类似于C语言中的逻辑表达式但是没有赋值操作)可以设置条件码:
指令 | 基于 | 描述 |
CMP S1,S2 cmpb cmpw cmpl cmpq | S2-S1 | 比较 比较字节 比较字 比较双字 比较四字 |
TEST S1,S2 testb testw testl testq | S1&S2 | 测试 测试字节 测试字 测试双字 测试四字 |
CMP指令和TEXT指令只设置条件码不改变寄存器。CMP指令根据两个操作数之差来设置条件码。TEST指令根据两个操作数的与运算来设置条件码。典型用法是两个操作数是一样的(例如,testq %rax,%rax用来检查%rax是负数,零还是正数),或其中一个操作数是一个掩码,用来指示那些位应该被测试。
3.5.2 访问条件码
条件码通常不会直接读取,常用方法有三种: 1)可以根据条件码的某种组合,将一个字节设置为0或1,2)可以条件跳转到程序的某个其它部分,3)可以有条件地传送数据。
结尾:
至于后续内容将在下一篇文章介绍