计算机组成原理-程序的机器代码表示


x86汇编语言指令基础

x86汇编语言是指所有结尾为86型号的cpu所能解析的语言。

指令的格式为:操作码 + 地址码。

以mov操作(mov指令功能,将源操作数s复制到目的操作数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

x86架构相关寄存器

若通用寄存器去掉开头的英文字母E,即AX,BX,CX,DX,此时大小为低16bit

若未指明主存读写长度时,默认32bit

例: mov eax, [ebx] 等价于 mov eax, dword ptr [ebx]

常用指令

以下的s均为源操作数(source),d为目的操作数(destination)。且目的操作数不可为常量,x86中两操作数不允许同时来自内存中

常见的算术运算指令

add d,s:add,加,把s加到d中。

sub d,s:subtract,减,d中减去s。

mul d,s:multiply,乘,无符号数d*s,乘积存入d。

imul d,s:有符号数d*s,乘积存入d。

div s:divide,除,无符号数除法,被除数会隐含放入到edx:eax(将被除数位扩展)中,然后edx:eax / s,商存入eax,余数存入edx。

idiv s:有符号数除法,被除数会隐含放入到edx:eax中,然后edx:eax / s,商存入eax,余数存入edx。

neg d:negative,将d取负数,结果存入d。

inc d:increase,d++。

dec d:decrease,d–。

常见的逻辑运算指令

and d,s:and,将d、s逐位相与,结果放回d。

or d,s:or,将d、s逐位相或,结果放回d

not d:not,将d逐位取反,结果放回d

xor d,s:exclusive or,将d、s逐位异或,结果放回d

shl d,s:shift left,将d逻辑左移s位,结果放回d(通常s是常量)

shr d,s:shift right,将d逻辑右移s位,结果放回d(通常s是常量)

其他指令

用于实现分支结构、循环结构的指令:cmp、test、jmp、jxxx

用于实现函数调用的指令:push、pop、call、ret

用于实现数据转移的指令:mov

AT&T格式和Intel格式

AT&T制定的汇编语言格式通常用于Unix、linux

Intel制定的汇编语言格式通常用于windows

选择语句的机器级表示

注:在Intel x86处理器中,程序计数器PC(Program Counter)通常被称为IP(Instruction Pointer)。

无条件转移指令——jmp

无条件转移指令格式: jmp <地址>

使用jmp会让PC无条件转移至<地址>,地址可以是常量、寄存器或来自主存。

例:

jmp 128:跳转到地址为128的位置

jmp eax:跳转到eax寄存器中所指的位置

jmp [999]:跳转到主存地址999中所指的位置

除了以上方法使用jmp,也可以使用“标号”锚定位置(用冒号结尾,名字可以自己取)。

条件转移指令——jxxx

条件转移指令一般要和cmp指令一起使用

cmp a,b   #比较a和b两个数
je <地址>   #jump when equal,若a==b跳转
jne <地址>  #jump when not equal,若a!=b跳转
jg <地址>   #jump when greater than,若a>b跳转
jl <地址>   #jump when less than, 若a>=b跳转
jle <地址>  #jump when less than or equal to,若a<=b跳转

例:

若有这么一段C语言代码

if (a>b){
    c=a;
} else {
    c = b;
}

则用汇编表示

mov eax,a   #将变量a存入eax
mov ebx,b   #将变量b存入ebx
cmp eax,ebx #比较a和b的值
jg NEXT     #若a>b,转移到NEXT:
mov ecx,ebx #假设ecx存储变量c,令c=b
jmp END     #无条件转移到END:
NEXT:
mov ecx,eax #假设ecx存储变量c,令 c=a
END:

循环语句的机器级表示

有这么一段循环代码

int i = 1;
int result = 0;
while(i<=100){
    result += i;
    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:         #跳出循环主体

用loop指令实现循环

x86中还提供了loop指令来实现循环

例:

for(int i=500; i>0; i--){
    //coding...
} 

用汇编实现

mov ecx,500     #用ecx作为循环计数器
Looptop:        #循环的开始
...
coding...
...
loop Looptop   #ecx--,若ecx!=0,跳转到Looptop

上方loop Looptop指令等价于dec ecx cmp ecx,0 jne Looptop三条指令。

注:loop是默认对ecx进行操作,用loop通用寄存器只能用ecx作为循环计数器

函数调用的机器级表示

在高级语言中,每个函数都有自己的栈区,称为栈帧,一个栈由若干个栈帧组成。目前正在执行的函数栈帧是一定位于栈顶。

函数调用x86汇编语言一般会通过call/ret指令来实现。

call/ret指令

用于实现子程序的调用及返回。

例:

int main(){
    //coding...
    add();
    return 0;
}

x86汇编实现

main:
 ...
 coding...
 ...
 call add  #这里调用了call函数
 ...
 
ret       #main函数结束

call指令的作用:

  • 将IP旧值压栈保存(效果相对于push IP)
  • 设置IP新值,无条件转移至被调用函数的第一条指令(效果相对于 jmp xxx)。

函数调用栈在内存中的位置

若有一台32位x86系统(x86系统默认以4字节为栈的操作单位),进程虚拟地址空间位4GB,高地址的1GB为操作系统内核区,低地址3GB为用户区,函数调用栈在用户区的高地址部分,具体如图所示。

若想使用栈,则需要用到EBP(堆栈基指针),ESP(堆栈顶指针)通用寄存器,EBP指向当前栈帧的“底部”,ESP指向当前栈帧的“顶部”

访问栈帧数据

使用push和pop指令访问

若要操作栈,则需使用pushpop指令,push会先让esp减4(即指向低地址的数据),后可以将立即数、寄存器或主存地址压入到栈顶,pop则是将栈顶元素出栈,然后放到寄存器或主存地址中,再让esp加4(指向高地址的数据)。

例:

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]中

add esp,8        #栈顶指针+8

即可以用mov指令,结合esp、ebp指针访问栈帧数据,用加法/减法指令,即sub/add修改栈顶指针esp的值。

函数调用时,切换栈帧

当函数调用时,可以通过push ebpmov ebp,esp来切换栈帧。

例:

有这么一段汇编代码

caller:
push ebp
mov ebp,esp
...
call add
...
leave
ret
...
...
add:
push ebp
mov ebp,esp
...
...
leave
ret

当上方代码运行到call add时,就会调用add函数,此时就需要切换栈帧,call指令一使用就会将IP旧值压栈,然后将新的IP值指向函数内的第一条指令,即push ebp,将栈低地址压入栈顶中,此时就是进行切换栈帧。

执行mov ebp,esp,将ebp栈底寄存器指向栈顶,就实现了切换。

注:push ebpmov ebp,esp可以精简为enter一条指令,效果相同

若想恢复原先的栈帧,使用mov esp,ebppop ebp即可实现,这两条也等价于leave指令。

栈帧中可能包含的内容

  • 通常会将局部变量集中存储在栈帧底部区域

  • 通常将调用参数集中存储在栈帧顶部区域

  • 栈帧最底部一定是上一层栈帧基址(ebp旧值)

  • 栈帧最顶部一定是返回地址(当前函数的栈帧除外)

使用ebp-4ebp-8等就可以访问局部变量,用esp+4esp+8等就可以访问调用参数

gcc编译器将每个栈帧大小设置为16B的整数倍(当前函数的栈帧除外),所以可能会出现中间空闲区域。


文章作者: LsWorld
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 LsWorld !
评论
  目录