AT&T汇编-x86

x86CPU下AT&T汇编

与AT&T汇编对应的是inter汇编,还有go的plan9

c data type typical 32-bit x86-32 x86-64
char 1 1 1
short 2 2 2
int 4 4 4
long 4 4 8
long long 8 8 8
float 4 4 4
double 8 8 8
long double 8 10/12 10/16
cahr * 4 4 8
  • at&t汇编语言数据格式
c声明 inter数据类型 汇编代码后缀 大小(byte)
char 字节 b 1
short w 2
int 双字 l 4
long 双字 l 4
long long 4
char * 双字 l 4
float 单精度 s 4
double 双精度 l 8
long double 扩展精度 t 10/12

mov? S,D

  • 将S传送到D
mov S,D S→D 传送
movb 传送字节
movw 传送字
movl 传送双字
movs S,D 符号扩展(S)→D 高位填充符号位
movsbw 扩展字节为字
movsbl 扩展字节为双字
movswl 扩展字为双字
movz S,D 零扩展(S)→D 高位充零
movzbw 扩展字节为字
movzbl 扩展字节为双字
movzwl 扩展字为双字
pushl S R[%esp]-4 → R[%esp] S→M[R[%esp]] 双字压栈
popl D M[R[%esp]] → D R[%esp]+4→R[%esp] 双字出栈
  • 注意栈是从高地址到低地址增长

允许操作的类型:

  1. 立即数:长整数
    • 如:¥0x400,$-533
  2. 寄存器:8个通用寄存器之一
    • %eax
    • %edx
    • %ecx
    • %ebx
    • %esi
    • %edi
    • %esp
    • %ebp
  3. 存储器:4个连续字节
  4. 不能从内存到内存

D(RB,RI,S) = Mem[Reg[RB]+S*REG[Ri]+D]

D:常量(地址偏移量)

Rb:基址寄存器:8个通用寄存器之一

RI:索引寄存器:%esp不作为索引寄存器,一般%ebp也不用作这个用途

S:比例因子 1,2,4,or 8

其他变形:

(RB,Ri) = Mem[Reg[RB]+REG[Ri]

D(RB,Ri) = Mem[Reg[RB]+REG[Ri]+D

(RB,Ri,S) = Mem[Reg[RB]+S*REG[Ri]

  • 地址计算指令leal
  • lea 不解引用, leaq (%rsp) %rax == movq %rsp %rax 而不是 movq (%rsp) %rax

leal src,dest

计算出来得地址赋给dest

  • 以寄存器之间操作举例
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
addl %eax,%ebx  # ebx += eax
subl %eax,%ebx  # ebx -= eax
imull %eax,%ebx # ebx *= eax
sall %eax,%ebx 	# ebx << eax 等价shll
sarl %eax,%ebx 	# ebx >> eax 算术右移
shrl %eax,%ebx 	# ebx >> eax 逻辑右移
xorl %eax,%ebx	# ebx ^= eax
andl %eax,%ebx	# ebx &= eax
orl %eax,%ebx 	# ebx |= eax
incl %eax	# eax++
decl %eax	# eax--
negl %eax	# eax = -eax
notl %eax	# eax = ~eax
  • 补充
1
2
3
4
5
6
7
imull S # R[%edx]:R[%eax]=S*R[%eax] 有符号乘,结果64位
mull  S # R[%edx]:R[%eax]=S*R[%eax] 无符号乘,结果64位
cltd S  # R[%edx]:R[%eax]=符号为扩展[%eax] 转换为8字节
idivl S # R[edx]=R[%edx]:R[%eax]%S
		# R[eax]=R[%edx]:R[%eax]/S  有符号除法,保存余数和商
divl S  # R[edx]=R[%edx]:R[%eax]%S
		# R[eax]=R[%edx]:R[%eax]/S  无符号除法,保存余数和商
name name
[%rax [%eax] ] [%r8 [%r8d] ]
[%rdx [%edx] ] [%r9 [%r9d] ]
[%rcx [%ecx] ] [%r10 [%r10d] ]
[%rbx [%ebx] ] [%r11 [%r11d] ]
[%rsi [%esi] ] [%r12 [%r12d] ]
[%rdi [%edi] ] [%r13 [%r13d] ]
[%rsp [%esp] ] [%r14 [%r14d] ]
[%rbp [%ebp] ] [%r15 [%r15d] ]
  • 兼容32位下的寄存器(e开头),仍然可以使用,%eax也兼容16位下ah,al寄存器

  • 64位传参:少于等于6个时放入寄存器rdi,rsi,rdx,rcx,r8,r9。多出的放入栈中

  • 32位传参:放在栈中

CF:carry flag

SF:sign flag

ZF:zero flag

OF:overflow flag

  • 条件码由算术指令隐含设置addl src,edst

如果产生进位,CF被设置,可以检测无符号数运算溢出

结果为0,ZF被设置

结果小于0,SF

补码运算溢出,OF,可以看作符号数运算溢出(两个大于0的数相加为负)

  • 比较指令cmpl src2,src1

cmpl b,a 类似 a-b

相等ZF置1

结果小于0,SF置1

溢出OF置1(a>0&&b<0&&(a-b)<0)||(a<0&&b>0&&(a-b)>0)

  • 测试指令testl src2,src1

testl b,a 类似a&b

当为0,ZF置1

当<0,SF置1

test使 CF,OF 置0

  • setX指令 读取当前条件码(组合)到目的字节寄存器
setX condition description
sete ZF equal/zero
setne ~ZF not equal/not zero
sets SF Negative
setns ~SF nonnegative
setg ~(SF^OF)&~ZF greater(signed)
setge ~(SF^OF) greater or equal(signed)
setl (SF^OF) less(signed)
setle (SF^OF)|ZF less or equal(signed)
seta ~CF&~ZF above(unsigned)
setb CF below(unsigned)
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
//a.c
int gt(int x,int y){
	return x>y;
}
/* gcc -S -m32 a.c -o a.asm32  摘要
	movl	8(%ebp), %eax    #get x   
	cmpl	12(%ebp), %eax	 #x-y
	setg	%al				 #取标记
	movzbl	%al, %eax		 #结果放到eax
 */
//-fno-omit-frame-pointer 不优化指针框架
  • 依赖当前条件码选择下一条执行语句
jX condition description
jmp 1 unconditional
je ZF equal/zero
jne ~ZF not equal/not zero
js SF negative
jns ~SF nonnegative
jg ~(SF^OF)&~ZF greater(signed)
jge ~(SF^OF) greater or equal(signed)
jl (SF^OF) less(signed)
jle (SF^OF)|ZF less or equal(signed)
ja ~CF&~ZF above(unsigned)
jb CF below(unsigned)
 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
//a.cpp
int absdiff(int x,int y){
	int res;
	if(x>y){
		res = x-y;
	}else{
		res = y-x;
	}
	return res;
}
/* gcc -S -m32 -fno-omit-frame-pointer a.c -o a.asm32
	movl	8(%ebp), %eax
	cmpl	12(%ebp), %eax
	jle	.L2
	movl	8(%ebp), %eax
	subl	12(%ebp), %eax
	movl	%eax, -4(%ebp)
	jmp	.L3
.L2:
	movl	12(%ebp), %eax
	subl	8(%ebp), %eax
	movl	%eax, -4(%ebp)
.L3:
	movl	-4(%ebp), %eax
	leave
	ret
*/
  • x86-64 下条件传送指令 比较新的机器有这个指令(比如i686)
  • cmovC src,dest
  • 如果C成立,将数据从src移动到dest

基地址是 .L62

通过jump table来进行跳转

取值比较稀疏通过二叉树表示

%esp存储栈顶地址,栈网低地址延伸

  • pushl src
  1. 从src取得操作数
  2. %esp=%esp-4,32位汇编下
  3. 数据写入栈顶地址%esp
  • popl dest
  1. 读取栈顶%esp数据
  2. %esp=%esp+4
  3. 数据写入dest
  • call label 将返回地址压入栈,跳转至label

  • 返回地址是指:call指令的下一条指令地址

  • ret 跳转至栈顶的返回地址

  • 存储的内容,自”顶“(栈顶)向下

    • 子过程参数
    • 局部变量,因为寄存器个数有限
    • 被保存的寄存器值
    • 父过程的栈帧起始地址(old %ebp
  • 栈帧的分配与释放

    • 进入过程后先分配栈帧空间
    • 过程返回时释放

%ebp指向当前栈帧起始地址,与%esp永远指向当前活跃栈帧的头尾

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
void swap(int *xp, int *yp)
{
    int t0 = *xp;
    int t1 = *yp;
    *xp = t1;
    *yp = t0;
}
int zip1 = 15213;
int zip2 = 91125;
void call_swap()
{
    swap(&zip1, &zip2);
}

 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
34
35
36
37
38
call_swap:
	#...
	pushl $zip2 #传参,从右往左压栈,变量加$表示取地址
	pushl $zip1
	call swap
	/*栈
		...     <- %ebp
		&zip2
		&zip1
		rtn adr  <- %esp
	*/
swap:
	pushl %ebp			#记录 old ebp
	movl %esp,%ebp		#设置自己的ebp
	pushl %ebx			#存一下寄存器信息
	
	movl 12(%ebp),%ecx
	movl 8(%ebp),%edx
	movl (%ecx),%eax
	movl (%edx),%ebx
	movl %eax,(%edx)
	movl %ebx,(%ecx)
	
	movl -4(%ebp),%ebx 	#恢复%ebx原来的值
	movl %ebp,%esp		#esp重置到ebp位置
	popl %ebp			#esp指向 rtn adr 返回地址
	ret					#根据esp返回
	#set up
	/*栈
		...     <- %ebp  
	+12	yp
	+8	xp
	+4	rtn adr  
	ebp	old %ebp  #pushl %ebp  <- %ebp #movl %esp,%ebp
	-4	old %ebx  #pushl %ebx
	
	*/
	#为什么我们只保存了old ebx?
  • 这是一个软件层的约定

  • 使用惯例—通用寄存器分为两类

    • 调用者负责保存
      • %eax %edx %ecx
      • %eax 用于保存过程返回值
    • 被调用者负责保存
      • %ebx %esi %edi
      • %ebp其实也是
    • 特殊
      • %ebp %esp

过程参数(不超过6个)通过寄存器传参,大于部分使用栈传

当参数少于7个时, 参数从左到右放入寄存器: rdi, rsi, rdx, rcx, r8, r9。 当参数为7个以上时, 前 6 个与前面一样, 但后面的依次从 “右向左” 放入栈中,即和32位汇编一样。

所有对于栈帧内容的访问都是基于%rsp,%rbp完全当作通用寄存器

x86-64下的栈帧有一些不用操作特性

  • 一次性分配整个帧
    • 将%rsp减去某个值(栈帧的大小)
    • 对于栈帧内容的访问都是基于%rsp完成
    • 可以延迟分配,可以访问(%esp)128字节的栈上空间
  • 释放简单
    • %rsp直接加上某个值

T A[L];

基本数据类型:T 数组长度:L

连续存储在大小为L*sizeof(T)字节的空间内

int a[5];

寄存器edx赋值为数组的起始地址,寄存器%eax赋值为下标,对应的元素地址为edx+4*eax

  • (%edx,%eax,4)
1
2
3
4
5
6
7
8
// a[5];
void zincr_p(int * z){
    int *zend = z+5;
    do{
        (*z)++;
        z++;
    }while(z!=zend);
}
1
2
3
4
5
6
7
//edx = z
movl $0,%eax		#i=0;
.L8:				#loop
addl $1,(%edx,%eax)	# *(z+i)++  注意汇编里面指针加都是加1
addl $4,%eax		# i+=4
cmpl $20,%eax		# compare i:20
jne  .L8			# if != ,goto loop
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
struct rec{
    int i;
    int a[3];
    int *p;
};
// 连续分配的内存区域,内部元素通过名字访问,元素类型可以不同
//访问
void set_i(struct rec *r,int val){
    r->i = val;
}
int * find_a(struct rec *r,int idx){
    return &r->a[idx];
}

相应汇编代码

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
# set_i
# %eax = val
# %edx = r
movl %eax,(%edx) #mem[r]=val

#find_a
# %ecx = idx
# %edx = r
leal 0(,%ecx,4),%eax # 4*idx
leal 4(%eax,%edx),eax # r+4*idx+4   r+4是a[0],r+4+idx*4是a[idx]

对齐的一般原则

  • 已知某种基本类型的大小为k字节
  • 那么,其存储地址必须是k的整数倍
  • x86-64的对齐要求基本上就是这样
    • 但是32位系统,linux与windows系统的要求都略有不同

为何需要对齐

  • 计算机访问内存一般是以内存块为单位的,块的大小是地址对齐的,如4,8,16字节对齐等
  • 如果数据访问地址跨越“块”边界会引起额外的内存访问

编译器的工作

  • 在结构的各个元素间插入额外空间来满足不同元素的对齐要求

x86-32下不同元素的对齐要求

  • 基本数据类型
    • 1 byte (char) 无要求
    • 2 byte (short)2字节对齐 地址最后一位为0
    • 4 byte (int,float,char *) 4字节对齐 地址后两位为0
    • 8 byte (double) win(及大多数) 8字节对齐,3位0,linux 四字节对齐,2位0
    • 12 byte (long double) win,linux 四字节对齐 两位0

x86-64下不同元素的对齐要求

  • 基本数据类型
    • 1,2,4 byte同32位一样
    • 8 bytes (double ,char *) win&linux 8字节对齐,地址后三位为0
    • 16 bytes (long double)linux 8位对齐

结构的存储对齐要求

  • 必须满足结构中各个元素的对齐要求
  • 结构自身的对齐要求等于其各个元素中对齐要求最高的那个,设为k字节
  • 结构的起始地址与结构长度必须是k的整数倍
1
2
3
4
5
6
7
8
9
#include <stdio.h>
#include <stdlib.h>
int main(){
    printf("hellow world\n");
    exit(0);
    return 0;
}
// gcc -S -O2 helloworld.c  这个是64位的汇编,我加-m32找不见头文件

 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
.file	"helloworld.c"
	.text
	.section	.rodata
.LC0:
	.string	"hello world"
	.text
	.globl	main
	.type	main, @function
main:
.LFB5:
	.cfi_startproc
	pushq	%rbp
	.cfi_def_cfa_offset 16
	.cfi_offset 6, -16
	movq	%rsp, %rbp
	.cfi_def_cfa_register 6
	leaq	.LC0(%rip), %rdi
	call	puts@PLT
	movl	$0, %edi
	call	exit@PLT
	.cfi_endproc
.LFE5:
	.size	main, .-main
	.ident	"GCC: (Ubuntu 7.3.0-27ubuntu1~18.04) 7.3.0"
	.section	.note.GNU-stack,"",@progbits
  • 汇编代码中以.开头的都是汇编指示(Directives),如.file.def.text等,用以指导汇编器如何进行汇编。其中.file .def均用于调试(可以将其忽略)
  • :结尾的字符串,如_main: 是用以表示变量或函数地址的符号(Symbol)
  • 其他均为汇编指令
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
.globl _main 	#指示汇编器表示符号_main是全局的
				#没有.globl标明的都是局部的
.text 		 	#代码段
.p2align 4,,15 	#指定下一行代码的对齐方式,参数1:按2的多少次幂对齐
									  #参数2:对齐是额外空间用什么数据填充
									  #参数3:最多允许额外填充多少字节
.section .rodata	#只读数据段
.LC0:
	.ascii "hello world\12\0" 		#.ascii是类型 文本字符串
									#.asciz 以空字符结尾的字符串
									#.byte 字节值
				#.double .float .single .int .long .octa .quad .short 
				#双精度	单精度			4byte       16	  8	    16
ages: #一行可以声明多个值
	.int 20,10,30,40

.section .bss #未初始化代码段
	.lcomm buffer,1000  #声明未初始化本地内存区域 symbol,size
	
.type power,@function #告诉linker power是一个函数

#.equ 用于把常量设置为可以在程序中使用的symbol
.equ factor,3
.equ LINUX_SYS_CALL,0x80

as -o my-object-file.o helloworld.s

  • -gstabs 产生带调试信息的object文件
  • 64位环境下使用32位汇编,加参数:–32

ld -o my-exe-file my-object-file.o

  • 可以有多个.o文件
  • 64位下生成32位可执行程序加参数:-m elf_i386

如果有什么找不见:sudo apt-get install g++-multilib

  • helloworld程序
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
.data #数据段
msg:
	.ascii "hellow world\n"
	len = .-msg   # . 表示当前地址
.text 
.globl _start #汇编程序入口
_start:
	movl $len,%edx
	movl $msg,%ecx
	movl $1,%ebx #系统输出
	movl $4,%eax
	int  $0x80
	
	movl $0,%ebx  #程序退出
	movl $1,%eax
	int $0x80
  • x86-linux 下的系统调用是通过中断指令(int 0X80)来实现的。
  • 在执行int $0x80指令时
    • 寄存器eax中存放的是系统调用的功能号,而传给系统调用的参数则必须按顺序放到寄存器ebx,ecx,edx,esi,edi中
    • 系统调用完成之后,返回值可以在eax中获得
    • 当系统调用需要参数大于5个,功能号保存在eax,全部参数依次存在一块连续内存中,同时在寄存器ebx中保存指向该内存区域的指针

查看系统调用功能号列表:locate unistd_32locate unistd_32

或者http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl

http://syscalls.kernelgrok.com/

  • 处理命令行参数: 将参数按行输出
 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
.text
.globl _start

_start:
	popl %ecx  #argc
vnext:
	popl %ecx 	#argv
	test %ecx,%ecx #空指针表示结束
	jz exit
	movl %ecx,%ebx #argv起始指针
	xorl %edx,%edx
strlen:
	movb (%ebx),%al
	inc %edx #edx++
	inc %ebx #ebx++
	test %al,%al
	jnz strlen
	movb $10,-1(%ebx) #10是换行符
	movl $4,%eax #系统调用sys_write
	movl $1,%ebx #参数1:fd  参数2:ecx字符串开始地址 参数3:edx 字符串长度
	int $0x80
	jmp vnext
exit:
	movl $1,%eax #系统调用号sys_exit
	xorl %ebx,%ebx #参数1:退出代码
	int $0x80
  • 调用libc库函数:打印cpu信息
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
.section .data
output:
	.asciz "The processor vendor id is '%s'\n" #.asciz 自动末尾加0
.section .bss #可读可写且没有初始化的数据区
	.lcomm buffer,12
.section .text
.globl _start
_start:
	movl $0,%eax
	cpuid  #参数放入eax,获取cpu特定信息,填0时,厂商id字符串反回到ebx,ecx,edx
		   #一共12个字符
	movl $buffer,%edi #buffer地址放入edi
	movl %ebx,(%edi)
	movl %edx,4(%edi)
	movl %ecx,8(%edi) #cpu信息放入buffer
	push $buffer #printf的参数,从右往左压栈
	push $output
	call printf #libc
	addl $8,%esp #栈中弹出参数
	push $0
	call exit
# ld -lc dynamic-linker /lib/ld-linux.so.2 -o cpuid.exe cpuid.o

go p88