G++中的内联汇编分为基本形式的内联汇编与扩展形式的内联汇编;毫无疑问,扩展形式的内联汇编更加复杂,也更加强大
__asm__与asm
两者是一样的,只不过ANSI C标准将asm作为关键字用于其他用途;所以为了与ANSIC兼容,还是使用__asm__;
__volatile__于volatile
告诉编译器,此处禁止优化,与__asm__一同使用,表示不优化内联汇编段
可以使用宏定义,来更加方便得使用内联汇编,如
#define _mBeginASM __asm__ __volatile__ (
#define _mEndASM );
基本形式的内联汇编语法如:
_mBeginASM
"汇编代码"
_mEndASM
G++会将"汇编代码"部分逐字插入到为程序生成的汇编代码中,所以应该在每一条指令后面手动添加’\n\t’;
语法形式:
_mBeginASM
"汇编代码模板"
:输出参数列表
:输入参数列表
:改动的寄存器列表
_mEndASM
总的作用机理就是:
感觉就像一个函数调用,'汇编代码模板’就是函数体,'输入参数列表’部分指定了函数的参数,'输出参数列表’部分指定了函数的返回值
cpu内部:
1. PC Program Counter
指令指针寄存器
指向下一条指令的地址
EIP(X86-32)或者
RIP(X86-64)
2. 寄存器与寄存器堆
Registers
在处理器CPU内部以名字来访问的快速存储单元
3. 条件状态码
Condition Codes
用于存储最近执行指令的结果状态信息
用于条件指令的判断执行
内存单元Memory:
以字节编码的连续存储空间
存储程序代码、数据、运行栈stack 以及操作系统数据
c 语言 数据类型 汇编代码后缀 大小(字节为单位)
char 字节 b byte 1
short 字 w word 2
int 双字 l 4
long int 双字 l 4
long long int 无 无 4
char* 双字 l 4
float 单精度 s 4
double 双精度 l 8
long double 扩展精度 t 10/12
c 语言代码:
int t = x+y // 两个整数(32位)
汇编代码:
addl 8(%ebp) %eax//l表示双字 8是位偏移量
操作数:
x: 寄存器 Register eax
y: 内存 Memory M[ebp+8] ebp是栈基址寄存器
t: 寄存器 Register eax
结果t保存在寄存器eax中
类似于表达:
x += y
或者:
int eax;
int* ebp;
eax += ebp[2];//这里按字节
movel 源地 目的地
将一个双字从源地移动到目的地
允许的操作数类型有:
立即数Imm:常整数
如: $0x400, $-533
可以用1,2或4个字节来表示
寄存器 Reg:
8个通用寄存器之一
%eax
%ebx
%ecx
%edx
%esi
%edi
%esp 栈顶
%ebp 栈底
存储器Mem:四个连续的字节
汇编 类似C语言
立即数--->寄存器 movl $0x41, %eax temp = 0x41;
立即数--->内存 movl $-43, (%eax) *p = -43;
寄存器--->寄存器 movl %eax, %edx temp2 = temp;
寄存器--->内存 movl %eax, (%edx) *p = temp;
内存 --->寄存器 movl (%eax), %edx temp = *p;
不允许内存到内存
1. 间接寻址 (R) Mem[Reg[R]]
寄存器R指定得内存地址
movl (%ecx), %eax
2. 基址+便宜量寻址 D(R) Mem[Reg[R] + D]
寄存器R指定内存的起始地址
常数D给出偏移地址
movl 8(%ebp), %ecx
交换两个数
C语言
void swap(int* xp, int* yp){
int t0 = *xp;
int t1 = *yp;
*xp = t1;
*yp = t0;
}
汇编语言分析
寄存器 变量:
%ecx yp
%edx xp
%eax t1
%ebx t0
对应汇编:
movl 12(%ebp),%ecx # ecx = yp 是地址 放到ecx寄存器中
movl 8(%ebp), %edx # edx = xp 是地址 放到edx寄存器中
movl (%ecx), %eax # eax = *yp t1 值 取寄存器中地址指向的内存地址中的内容放入 寄存器eax中
movl (%edx), %ebx # ebx = *xp t0 值 取寄存器中地址指向的内存地址中的内容放入 寄存器ebx中
movl %eax, (%edx) # *xp = eax 交换内容放入原来内存指向得地址中
movl %ebx, (%ecx) # *yp = ebx
ebp 是函数栈 基地址
ebp+8 的位置 存储 指针xp 指向内存的一个地址
ebp+12 的位置 存储 指针yp 指向内存的一个地址
常见形式:
D(Rb,Ri,S) Mem[Reg[Rb] + S*Reg[Ri] + D]
D: 常量(地址偏移量)
Rb: 基址寄存器:8个通用寄存器之一
Ri: 索引寄存器: %esp不作为索引寄存器
一般%ebp也不做这个用途
S: 比例因子, 1,2,4,8
其他变形:
D(Rb,Ri) Mem[Reg[Rb] + Reg[Ri] + D]
(Rb,Ri) Mem[ Reg[Rb] + Reg[Ri] ]
(Rb,Ri) Mem[Reg[Rb] + S*Reg[Ri]]
leal src, dest
src 是地址计算表达式子
计算出来得地址赋给 dest
使用实例:
地址计算,无需访问内存 auto *p = &x[i];
进行x+k*y这一类型得整数计算,k = 1,2,4,8
addl src,dest # dest = dest + src #加法
subl src,dest # dest = dest - src #减法
imull src,dest # dest = dest * src #乘法
sall src,dest # dest = dest << src #左移位 等价于shll
sarl src,dest # dest = dest >> src #算术右移位 补 被移动数的最高位
shrl src,dest # dest = dest >> src #逻辑右移位 左边单纯补 0
xorl src,dest # dest = dest ^ src #按位异或
andl src,dest # dest = dest & src #按位与
orl src,dest # dest = dest | src #按位或
incl dest # dest = dest + 1 #++ 自增1
decl dest # dest = dest - 1 #-- 自减1
negl dest # dest = - dest # 取非
notl dest # dest = ~ dest # 取反
c语言:
int temp(int x, int y){
int t1 = x+y;
int t2 = z+t1;
int t3 = x+4;
int t4 = y*48;
int t5 = t3+t4;
int ret = t2*t5
return ret;
}
汇编代码:
movl 8(%ebp), %eax # eax = x
movl 12(%ebp), %edx # edx = y
leal (%eax, %edx), %ecx # t1 = ecx = x+y
addl 16(%ebp), %ecx # t2 = ecx = z+t1
leal (%edx,%edx,2), %edx # edx = 3*y
sall $4, %edx # t4 = edx = 2^4 * 3 * y =16*3*y = 48*y
leal 4(%eax,%edx), %eax # t5 = eax =4+x+t4
imul %ecx, %eax # ret = eax = t2*t5
C语言:
int logical_(int x, int y){
int t1 = x^y;
int t2 = t1>>17;
int mask = (1<<13) - 7;
int ret = t2 & mask;
return ret;
}
汇编语言:
movl 8(%ebp), %eax # eax = x
xorl 12(%ebp), %eax # t1 = eax = x^y
sarl $17, %eax # t2 = eax = t1 >> 12
andl $8185, %eax # ret = eax = t2 & 8185 2^13-7 = 8185 这里编译器会算出来
x86-32 x86-64
long int 4 8
char* 4 8 内存地址编号长度
addl src,dest t =a+b
CF Carry Flag 进位标志 可用于无符号整数运算的溢出
SF Sign Flag 符号位标志 结果<0 的话,被设置为1
ZF Zero Flag 0结果标志 结果为0的话,被设置为1
OF Overflow Flag 溢出标志 if a>0 & b>0 & t<0 被设置为1
compare b,a a-b
if a == b , ZF =1
if a < b , SF=1
if (a>0 && b<0 && (a-b)<0)||(a<0 && b>0 && (a-b)>0) OF=1
testl s2,s1
testq s2,s1
计算 s1 & s2并设置相应的条件码,不改变目的操作数
testl b,a 计算a&b
if a&b ==0, ZF =1
if a&b < 0, SF =1
CF=0,OF=0
sete 相等 结果为0 ZF =1时
setne 不相等 ZF =0
sets 结果<0 SF =1
setns 结果>=0 SF=0
有符号数:
setg 大于
setge 大于等于
setl 小于
setle 小于等于
无符号数
setb 小与 below
seta 大于 ablove
c语言:
int gt(int x, int y){
return x>y;
}
汇编语言:
movl 12(%ebp), %eax # eax = y
cmpl %eax, 8(%ebp) # compare x:y x-y
setg %al # al寄存器是eax的低八位得名字 al = x>y
movzbl #al, %eax # 8位扩展到16位
jmp 无条件跳转
je 相等
jne 不相等
js 结果<0
jns 结果>=0
有符号
jg 大于
jge 大于等于
jl 小于
jle 小与等与
无符号
ja 大于
jb 小于
c语言:
int abs_(int x, int y){
int rest;
if(x>y){
rest = x - y;
}
else{
rest = y-x;
}
return rest;
}
汇编语言:
movl 8(%ebp), %edx # edx = x
movl 12(%ebp), %eax # eax = y
cmpl %eax, %edx # x - y
jle .L7 # x <=y 跳转到.L7
subl %eax, %edx # x>y , 计算rest = x-y
movl %edx, %eax # 结果放在 寄存器eax指向的地址
.L8:
leave
ret
.L7:
subl %edx, %eax # x <=y 跳转到.L7 计算 y-x
jmp L8
for循环:
for(Init初始条件; 判断测试条件Test; 更新量Update)
循环体Body;
转换到 While-do结构:
Init初始条件;
while(判断测试条件Test){
循环体Body;
更新量Update;
}
再转换成go-to结构:
Init初始条件;
goto middle; # 无条件跳转 jmp
loop:
循环体Body;
更新量Update;
middle:
if(判断测试条件Test)
goto loop;
c语言版本:
long switch_eg(long x, long y, long z){
long w = 1;
switch(x){
case 1:
w = y*z;
break;
case 2:
w = y/z;
break;
case 3:
w +=z;
break;
case 5:
case 6:
w -= z;
break;
defult: // 这里 case 0 和 case 4 和其他情况
w = 2;
}
return w;
}
汇编语言版本:
pushl %ebp # 保存 寄存器老的 ebp的值
movl %esp, %ebp # 函数堆栈 栈顶指针 %esp 存放在%ebp
pushl %ebx # 保存 寄存器ebx的值
movl 8(%ebp), %ebx # 函数堆栈距离栈顶 偏移8位处存放x的值
movl 12(%ebp), %eax # y
movl 16(%ebp), %ecx # z
cmpl $6, %ebx # x - 6
jbe .L11 # X <= 6 才进入正常的 case
.L8: # default 部分
movl $2, %eax # w = 2
jmp .L12 # break
.L11:
jmp *.L7(,%ebx,4) %访问.L7段表 对应的 case block
.section .rodata
.L7:
.long .L8 # case 0 情况 进入 default
.long .L3 # case 1 情况
.long .L4 # case 2 情况
.long .L9 # case 3 情况
.long .L8 # case 4 情况 进入 default
.long .L6 # case 5 情况 case 6
.long .L6 # case 6 情况 进入 default
.L12: # break
popl %ebx # 后进先出
popl %ebp # 先进后出
ret # 函数段返回
.L3: # case 1
imull %ecx, %eax # w = w*z
jmp .L12 # break
.L4: # case 2
movl %eax, %edx
sarl $31, %edx
idivl %ecx # w =y/z
addl %ecx, %eax # case 3 w += z
jmp .L12 # break
.L9: # case 3
movl %1, %eax # w = 1
addl %ecx, %eax # w +=z
jmp .L12 # break
.L6: # case 6 case 5
movl $1, %eax # w = 1
subl %ecx, %eax # w -= Z
jmp .L12 # break
栈---水桶结构的一块内存区域---->只有一个出口(栈底%ebp (桶口)高地址(计算机地址逆向生长))
先进后出后进先出 FILO
寄存器 %esp 存储栈顶地址(下部)
寄存器 %ebp 存储栈底地址(上部)
注意 %esp %ebp 始终指向当前正在运行的活动的函数过程栈
局部变量
返回地址
临时空间
1. 进入过程 先分配栈帧空间
set-up code
2. 过程返回时 释放栈帧空间
finish code
水面栈顶%esp会上什(步进单位为4个内存地址块)
pushl src # 从src取得操作数
# %esp = %esp-4 栈顶上升 水面上什
水面栈顶%esp会下降
popl Dest # 读取栈顶数据(%esp) 放入 目标位置Dest
# %esp = %esp+4 栈顶下降 水面下降
过程调用指令:
call label #将返回地址(call指令的下一条指令地址(%eip)压入栈),跳转至label继续执行
过程返回指令
ret #跳转至栈顶的返回地址
8个寄存器:
两个特殊寄存器:
调用者和被调用者可能都需要保存
始终指向当前活动的堆栈上两端
%ebp 栈底指针
%esp 栈顶指针
三个由调用者保存:
%eax 常用于保存过程的返回值的
%ecx
%edx
三个由被调用者保存:
%ebx
%esi
%edi
c语言版本:
int rfact(int x){
int rval;
// 递归出口
if(x<=1) return 1;
// 递归调用
rval = rfact(x-1);
return rval * x;
}
汇编语言版本:
调用者栈情况 前 %ebp <- %ebp 栈底
前 %ebx
变量x 父过程调用的参数
返回地址 adr <- %esp 栈顶
# 进入过程 先分配栈帧空间
被调用者(函数rfact) 老的 %ebp 第一步需要保存%ebp pushl %ebp 指向 前%ebp <- %esp 栈顶 栈顶自动调整
第二步 movl %esp, %ebp <- %ebp 栈第指针也指向了栈顶 (栈基址)
老的 %ebx 第三步 pushl %ebx <- %esp 栈顶 栈顶自动调整
(用来存储函数内的变量值,之前的值需要先保存)
主体代码:
movl 8(%ebp), %ebx # ebx = x
cmpl $1, %ebx # compare x : 1 , 计算 x - 1
jle .L78 # x <= 1 直接跳转到递归出口 .L78
leal -1(%ebx), %eax # eax = x-1 被调用者这里需要用到 %eax 作为过程的返回值的
pushl %eax # push x-1 入栈保存 这里用作为调用者需要保存 %eax
call rfact # rval = rfact(x-1), 函数返回值保存在 %eax中
imull %ebx, %eax # rval * x
jmp .L79 # 跳转到 done
.L 78:
movl $1, %eax # return eax = 1
# 返回代码 过程返回时 释放栈帧空间
.L79:
movl -4(%ebp), %ebx # 复原老的 %ebx 的值
movl %ebp, %esp # 栈顶指针 %esp 也指向%ebp指向的位置
popl %ebp # 恢复老的 %ebp
ret
x86-32 有 8个32位的寄存器
8个寄存器:
两个特殊寄存器:
调用者和被调用者可能都需要保存
始终指向当前活动的堆栈上两端
%ebp 栈底指针
%esp 栈顶指针
三个由调用者保存:
%eax 常用于保存过程的返回值的 累加器 计算操作数和存放结果数据
%ecx 计数寄存器
%edx 数据寄存器
三个由被调用者保存:
%ebx 基础寄存器 指向DS数据段的数据指针
%esi 源索引
%edi 目的索引
x86-64 有 16个64位的寄存器:
与 x86-32兼容的8个64位的寄存器
%rax 低16位也叫 %eax eax 低16位称为ax,ax的高8位称为ah,低8位称为al
%rbx %ebx ebx 低16位称为bx,bx的高8位称为bh,低8位称为bl
%rcx %ecx ecx 低16位称为cx,cx的高8位称为ch,低8位称为cl
%rdx %edx edx 低16位称为dx,dx的高8位称为dh,低8位称为dl
%rsi %esi esi 低16位称为si
%rdi %edi edi 低16位称为di
%rsp %esp esp 低16位称为sp
%rbp %ebp ebp 低16位称为bp
新增加的8个64位的寄存器:
%r8 低16位也叫 %r8d
%r9 %r9d
%r10 %r10d
%r11 %r11d
%r12 %r12d
%r13 %r13d
%r14 %r14d
%r15 %r15d
CS 代码段 存储指令和执行的地方
DS, ES, FS, GS 数据段
SS 堆栈段 当前程序存储堆栈的地方
访问:
c语言:
typedef int array_i_5[5];// 定义一个长度为5的整数数组类型 array_i_5
array_i_5 cmu = {1, 2, 5, 3, 4};// 定义一个数组cmu 函数五个整形变量
cum[index];//可以获取相应索引位置处的 元素
汇编语言:
# %edx = cmu 数组首地址
# %eax = index 索引
movl (%edx, %eax, 4), %eax # cmu[index] 4为一个整形类型所占据的直接数量
数组循环遍历修改
c语言:
void temp(array_i_5 z){
int i;
for (i=0; i<5; i++)
z[i]++;// 数组各元素自增1
}
汇编语言:
movl $0, %eax # 循环变量 %eax = i
.L4:
addl $1, (%edx, %eax, 4) # z[i]++
addl $1, %eax # i++
cmpl $5, %eax # compare i:5
jne .L4 # i < 5 循环
int arr_i_i[row_index][col_index];
一个int类型占据4个字节地址
则 arr_i_i[y][x] 的实际地址为 arr_i_i + y*row_index*col_index + 4*x
例如一个 arrii[4][5]的二维数组,每一行占有 4*5=20个字节
那么 arrii[i][j] 的地址为 arrii + i*20 + 4*j
arrii + 4*(i+4*i) + 4*j
相关汇编代码:
先计算列地址
leal 0(,%ecx,4), %edx # 4*j
leal (%eax, %eax, 4), %eax # 5 *i
movl arrii(%edx, %eax, 4), %eax # arrii + 4*j + 4*(i+4*i)
//helloworld.c
#include
#include
int main(){
printf("Hello World\n");
exit(0);
return 0;//这里就不会编译了
}
// gcc 编译
gcc -S -O2 helloworld.c
-S 生成汇编代码(与系统类型相同) helloworld.s
-m32 64位系统生成32位汇编
-On -O2 2级编译优化
// asm
movl $LC0 (%esp) #设置过程调用参数 字符串首地址 放在栈顶对应的地址
call _puts
movl $0, (%esp) #设置过程调用参数
call _exit
.text #代码段
.p2align 4,,15 #对齐方式 按16 = 2^4字节对齐,填充,, 0, 最大填充15个字节
.section .rdata "dr" #只读数据段
LC0:# 字符串的起始地址
.ascii "Hello World\12\0"
编译:
//gcc -S -O2 helloworld.c
as -o my-object-file.o helloworld.s
# -gstabs 产生带调试信息的object文件
# 64位环境下添加命令行 --32 生成 32位目标文件
链接成执行程序
ld -o my-exe-file my-object-file.o
# 64位环境下添加命令行 -m elf_i386 生成 32位目标文件
helloworld.s
.data #数据段
msg: # 字符 首地址
.ascii "Hello World\n"
len = .-msg # 字符长度 . 表示当前地址
.text # 代码段
.globl _start # 汇编程序入口地址,如同C语言的main函数
_start:
movl $len, %edx # 字符串长度 字节数量
movl $msg, %ecx # 字符串存储位置的起始地址
movl $1, %ebx # 系统输出(write 系统调用1)
movl $4, %eax #
int $0x80 # 中断 来指向系统调用
# eax存放的是系统调用的功能号
# ebx 为参数 为1时 是 write 显示器输出
# edx 字符串长度 字节数量
# ecx 字符串存储位置的起始地址
movl $0, %ebx # 系统调用(程序退出 0)
movl $4, eax
int $0x80 # 中断 来指向系统调用