AT&T汇编-x86
x86CPU下AT&T汇编
与AT&T汇编对应的是inter汇编,还有go的plan9
c语言中基本数据类型得大小(byets)
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)
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] | 双字出栈 |
- 注意栈是从高地址到低地址增长
允许操作的类型:
- 立即数:长整数
- 如:¥0x400,$-533
- 寄存器:8个通用寄存器之一
- %eax
- %edx
- %ecx
- %ebx
- %esi
- %edi
- %esp
- %ebp
- 存储器: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
整数计算指令
- 以寄存器之间操作举例
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
- 补充
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 无符号除法,保存余数和商
x86-64的通用寄存器
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) |
//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) |
//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
switch的汇编表示
表结构
基地址是 .L62
通过jump table来进行跳转
取值比较稀疏通过二叉树表示
程序运行栈
%esp
存储栈顶地址,栈网低地址延伸
pushl src
- 从src取得操作数
%esp=%esp-4
,32位汇编下- 数据写入栈顶地址
%esp
popl dest
- 读取栈顶
%esp
数据 %esp=%esp+4
- 数据写入dest
过程调用指令:call
-
call label
将返回地址压入栈,跳转至label -
返回地址是指:call指令的下一条指令地址
-
ret
跳转至栈顶的返回地址
栈帧:%ebp
-
存储的内容,自”顶“(栈顶)向下
- 子过程参数
- 局部变量,因为寄存器个数有限
- 被保存的寄存器值
- 父过程的栈帧起始地址(old
%ebp
)
-
栈帧的分配与释放
- 进入过程后先分配栈帧空间
- 过程返回时释放
%ebp
指向当前栈帧起始地址,与%esp
永远指向当前活跃栈帧的头尾
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);
}
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
- 调用者负责保存
x86-64过程调用与运行栈
过程参数(不超过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)
// a[5];
void zincr_p(int * z){
int *zend = z+5;
do{
(*z)++;
z++;
}while(z!=zend);
}
//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
8086汇编与c语言
结构struct
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];
}
相应汇编代码
# 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的整数倍
80x86-32汇编编程
hello world
#include <stdio.h>
#include <stdlib.h>
int main(){
printf("hellow world\n");
exit(0);
return 0;
}
// gcc -S -O2 helloworld.c 这个是64位的汇编,我加-m32找不见头文件
.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) - 其他均为汇编指令
.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
linux汇编命令&系统调用
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程序
.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_32
或locate unistd_32
或者http://codelab.shiyanlou.com/xref/linux-3.18.6/arch/x86/syscalls/syscall_32.tbl
http://syscalls.kernelgrok.com/
- 处理命令行参数: 将参数按行输出
.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信息
.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