# 高级语言与机器级代码之间的对应
# x86 汇编语言指令基础
指令的作用:改变程序执行流、处理数据
指令格式:操作码 + 地址码
数据在哪:寄存器、主存、指令
- mov 指令功能:将源操作数 s 复制到目的操作 d 所指的位置
- mov 目的操作数 d,源操作数
- mov eax,ebx:将寄存器 ebx 的值复制到寄存器 eax
- mov eax,5:将立即数 5 复制到寄存器 eax
- mov eax,dword ptr [af996h]:将内存地址 af996h 所指的 32bit 复制到寄存器 eax
- mov byte ptr [af996h],5:将立即数 5 复制到内存地址 af996h 所指的一字节中
指明内存读写长度:
- dword ptr—— 双字,32bit
- word ptr—— 单字,16bit
- byte ptr—— 字节,8bit
- 若未指明主存读写长度,默认 32bit
x86 架构 CPU 有哪些寄存器(每个寄存器都是 32bit):
- 通用寄存器:EAX,EBX,ECX,EDX(X = 未知)
- 变址寄存器 (ESI,EDI,S=Source,D=Destination):变址寄存器可用于线性表、字符串的处理
- 堆栈寄存器:用于实现函数调用
- 堆栈基指针 (EBP)
- 堆栈顶指针 (ESP)
# 常用 x86 汇编指令
# 算数运算指令
算术运算:加、减、乘、除、取负数、自增 ++、自减 --
- 加:add d,s (计算 d+s,结果存入 d)
- 减:sub d,s (计算 d-s,结果存入 d)
- 乘:mul d,s (无符号数 d*s,乘积存入 d);imul d,s (有符号数 d*s,乘积存入 d)
- 除:div d,s (无符号数除法 edx:eax/s,商存入 eax,余数存入 edx);idiv d,s (有符号数除法 edx:eax/s,商存入 eax,余数存入 edx)
- 取负数:neg d (将 d 取负数,存入 d)
- 自增 ++:inc d (将 d++,结果存入 d)
- 自减 --:dec d (将 d--,结果存入 d)
# 逻辑运算指令
逻辑运算:与、或、非、异或、左移、右移
- 与:and d,s (将 d,s 逐位相与,结果放到 d)
- 或:or d,s (将 d,s 逐位相或,结果放到 d)
- 非:not d (将 d 逐位取反,结果放到 d)
- 异或:xor d,s (将 d,s 逐位异或,结果放到 d)
- 左移:shl d,s (将 d 逻辑左移 s 位,结果放到 d (通常 s 为常量))
- 右移:shr d,s (将 d 逻辑右移 s 位,结果放到 d (通常 s 为常量))
# 其他指令
用于实现分支结构、循环结构的指令:cmp,test,jmp,jxxx
用于实现函数调用的指令:push,pop,call,ret
用于实现数据转移的指令:mov
# AT&T 格式和 Intel 格式
AT&T 格式:Unix、Linux 的常用格式
Intel 格式:Windows 的常用格式
AT&T 格式 | Intel 格式 | |
---|---|---|
目的操作数 d、源操作数 s | op s,d 注:源操作数在左,目的操作数在右 | op d,s 注:源操作数在右,目的操作数在左 |
寄存器的表示 | mov % ebx,% eax 注:寄存器名之前必须加 % | mov eax,ebx 注:直接写寄存器名即可 |
立即数的表示 | mov $985,% eax 注:立即数前必须加 $ | mov eax,985 注:直接写数字即可 |
主存地址的表示 | mov % eax,(af996h) 注:用小括号 | mov [af996h],eax 注:用中括号 |
读写长度的表示 | movb,movw,movl,addb 注:指令后加 b,w,l 分别表示读写长度为 byte,word,dword | move byte ptr,move word ptr,move dword ptr,add byte ptr 注:在主存地址前说明读写长度 byte,word,dword |
主存地址偏移量的表示 | movl -8 (% ebx),% eax 注:偏移量 (基址) movl 4 (% ebx,% ecx,32),% eax 注:偏移量 (基址,变址,比例因子) | mov eax,[ebx,-8] 注:[基址 + 偏移量] mov eax,[ebx+ecx*32+4] 注:[基址 + 变址 * 比例因子 + 偏移量] |
# 选择语句机器级表示
无条件转移指令:
jmp <地址>(PC 无条件转移至 < 地址 >)
jmp 128(<地址> 可以用常数给出)
jmp eax(<地址> 可以来源于寄存器)
jmp [999](< 地址 > 可以来源于主存)
jmp NEXT(<地址> 可以用标号锚定)
可以用标号锚定位置,如 NEXT:(特征 —— 有冒号,名字可以自己取)
条件转移指令 ——jxxx
- je <地址>(jump when equal,若 a==b 则跳转)
- jne <地址>(jump when not equal,若 a!=b 则跳转)
- jg <地址>(jump when greater than,若 a>b 则跳转)
- jge <地址>(jump when greater than or equal to,若 a>=b 则跳转)
- jl <地址>(jump when less than,若 a<b 则跳转)
- jle <地址>(jump when less than or equal to,若 a<=b 则跳转)
条件转移指令一般要和 cmp 指令一起使用,cmp a,b(比较 a,b 两个数)
套路:
cmp eax,ebx #比较寄存器eax和ebx里的值
jg NEXT: #若eax>ebx,则跳转到NEXT:
实例:
//C语言
if(a>b){
c=a;
} else {
c=b;
}
转为汇编语言:
mov eax,7 #假设变量a==7,存入eax
mov ebx,6 #假设变量b==6,存入ebx
cmp eax,ebx #比较变量a和b
jg NEXT: #若a>b,转移到NEXT:
mov ecx,eax #假设用ecx存储变量c,令c=b
jmp END: #无条件跳转到END:
NEXT:
mov ecx,eax #假设用ecx存储变量c,令c=a
END:
扩展:cmp 指令底层原理
本质进行减法,生成 OF (溢出标志),SF (符号标志),ZF (零标志),CF (进位 / 错位标志),根据这些标志来判断哪边大,哪边小
# 循环语句的机器级表示
例子:
//C语言
int result=0;
for(int i=1;i<=100;i++){
result+=i;
}//求1+2+3+...+100的和
转为汇编语言:
mov eax,0 #用eax保存result,初值为0
mov edx,1 #用edx保存i,初值为1
cmp edx,100 #比较i和100
jg L2 #若i>100,跳转到L2执行
L1: #循环主题
add eax,edx #实现result+=i
inc edx #inc自增指令,实现i++
cmp edx,100 #i和100
jle L1 #若i<=100,跳转到L1执行
L2: #跳出循环主体
用转移类指令实现循环,需要 4 个部分组成:
- 循环前的初始化
- 是否直接跳过循环
- 循环主体
- 是否继续循环
用 loop 指令实现循环
for(int i=500;i>0;i--){
//do something
} //循环500轮
转为汇编
mov ecx,500 #用ecx作为循环计数器
Looptop: #循环的开始
...
做某些处理
...
loop LoopTop #ecx--,若ecx!=0,跳转到Looptop
理论上,用 loop 指令实现的功能一定能用条件转移指令实现
使用 loop 指令可能会使代码更清晰简洁
loopnz—— 当 ecx!=0 && ZF==0 时,继续循环
loopz—— 当 ecx!=0 && ZF==1 时,继续循环
# 函数调用机器级表示 (Call,ret 指令)
函数的栈帧:保存函数大括号定义的局部变量、保存函数调用相关的信息
当前正在执行的函数栈帧,位于栈顶
x86 汇编语言的函数调用
int add(int x,int y){
return x+y;
}
int caller(){
int temp1=125;
int temp2=80;
int sum=add(temp1,temp2);
return sum
}
转为汇编语言
caller:
push ebp
mov ebp,esp
sub esp,24
mov [ebp-12],125
mov [ebp-8],80
mov eax,[ebp-8]
mov [esp+4],eax
mov eax,[ebp-12]
mov esp,eax
call add
mov [ebp-4],eax
mov eax,[ebp-4]
leave
ret
add:
push ebp
mov ebp,esp
mov eax,[ebp+12]
mov eax,[ebp+8]
add eax,edx
leave
ret
函数调用指令:call <函数名>
函数返回指令:ret
call 指令作用:
- 将 IP 旧值压栈保存 (保存在函数的栈帧顶部)
- 设置 IP 新值,无条件转移至被调用函数的第一条指令
ret 指令作用:从函数的栈帧顶部找到 IP 旧值,将其出栈并恢复 IP 寄存器
# 如何访问栈帧
标记帧栈范围:EBP、ESP 寄存器
ebp:指向当前栈帧的 "底部",esp:指向当前栈帧的 "顶部"
注:在 x86 系统当中,默认以 4 字节为栈的操作单位
对栈帧内数据的访问,都是基于 ebp,esp 进行的
# 访问栈帧数据:push、pop 指令
push、pop 指令实现入栈、出栈操作,x86 默认以 4 字节为单位,指令格式如下:
Push x—— 先让 esp 减 4,再将 x 放入
Pop y—— 栈顶元素出栈写入 y,在让 esp 加 4
注:x 可以是立即数、寄存器、主存地址;y 可以是寄存器、主存地址
push eax #将寄存器eax的值压栈
push 985 #将立即数985压栈
push [ebp+8] #将主存地址[ebp+8]里的数据压栈
pop eax #栈顶元素出栈,写入寄存器eax
pop [ebp+8] #栈顶元素出栈,写入主存地址[ebp+8]
# 访问栈帧数据:mov 指令
sub esp,12 #栈顶指针-12
mov [esp+8],eax #将eax的值复制到主存[esp+8]
mov [esp+4],985 #将985复制到主存[esp+4]
mov eax,[ebp+8] #将主存[esp+8]的值复制到eax
mov [esp],eax #将eax的值复制到主存[esp]
add esp,8 #栈顶指针+8
- 可以用 mov 指令,结合 esp、ebp 指针访问栈帧数据
- 可以用减法 / 加法指令,即 sub/add 修改栈顶指针 esp 的值
# 如何切换栈帧
call 指令作用:
- 将 IP 旧值压栈保存 (效果相当于 (push IP))
- 设置 IP 新值,无条件转移至被调用函数的第一条指令 (效果相当于 jmp add)
add:
push ebp #保存上一层函数的栈帧基址(esp旧值)
mov ebp,esp #设置当前函数的栈帧基址(ebp新值)
# 这两条指令等价于enter指令
mov esp,ebp #让esp指向当前栈帧的底部
pop ebp #将esp所指元素出栈,写入寄存器ebp
# 这两条指令等价于leave指令
ret 指令作用:
从函数的栈帧顶部找到 IP 旧值,将其出栈并恢复 IP 寄存器
# 如何传递参数与返回值
栈帧底部一定是上一层栈帧基址 (ebp 旧址)
栈帧顶部一定是返回地址 (当前函数的栈帧除外)
通常将局部变量集中存储在栈帧底部区域,C 语言中越靠前定义的局部变量越靠近栈顶
通常将调用参数集中存储在栈帧顶部区域,参数列表中越靠前的参数越靠近栈顶
gcc 编译器将每个栈帧大小设置为 16B 的整数倍 (当前函数的栈帧除外),因此栈帧内可能出现空闲未使用区域
自底向顶的栈帧包含内容:
# CISC 和 RISC
CISC:Complex Instruction Set Computer(复杂指令集计算机)
设计思路:一条指令完成一个复杂的基本功能
代表架构:x86 架构,主要用于笔记本、台式机等
80-20 规律:典型程序中 80% 的语句仅仅使用处理机中 20% 的指令
RISC:Reduced Instruction Set Computer(精简指令集计算机)
- 设计思路:一条指令完成一个基本 "动作";多条指令组合完成一个复杂的基本功能
- 代表:ARM 架构,主要用于手机、平板等
例子:
设计一套能实现整数、矩阵加 / 减 / 乘运算的指令集:
CISC 思路:除了提供整数的加减乘除指令之外,还提供矩阵的加法指令、矩阵的减法指令、矩阵的乘法指令
一条指令可以由一个专门的电路完成
有的复杂指令用纯硬件实现很困难 -> 采用存储程序的设计思想,由一个比较通用的电路配合存储部件完成一条指令
RISC 思路:只提供整数的加减乘指令
一条指令一个电路,电路设计相对简单,功耗更低。"并行","流水线"
CISC | RISC | |
---|---|---|
指令系统 | 复杂,庞大 | 短小,精简 |
指令数目 | 一般大于 200 条 | 一般小于 100 条 |
指令字长 | 不固定 | 定长 |
可访存指令 | 不加限制 | 只有 Load/Store 指令 |
各种指令执行时间 | 相差较大 | 绝大多数在一个周期内完成 |
各种指令使用频度 | 相差很大 | 都比较常用 |
通用寄存器数量 | 很少 | 多 |
目标代码 | 难以用优化编译生成高效的目标代码程序 | 采用优化的编译程序,生成代码较为高效 |
控制方式 | 绝大多数为微程序控制 | 绝大多数为组合逻辑控制 |
指令流水线 | 可以通过一定方式实现 | 必须实现 |
乘法指令可以访存,一定是 CISC