程序的机器级表示-基本操作

程序编码

机器级代码

理解机器级代码有2种抽象需要理解。

  • 指令集架构:来定义机器级程序的格式以及行为。定义了处理器的状态,指令格式,以及每条指令的影响
  • 机器级指令使用的是虚拟内存。这个是编译器来决定的。具体把虚拟内存翻译成物理内存运行时有专门的硬件(MMU)来处理。目前的x86-64的虚拟内存中,地址的高16位被设置成0,所以寻址范围的2^{48},大小在256T.
long mult2(long x,long y); 
void multstore(long x,long y,long *dst){
    long t = mult2(x,y);
    *dst = t;
}
使用gcc -Og -S 产生的汇编代码(去除掉伪指令)
multstore:
#%rdi %rsi %rdx分别存储着x,y,以及dst的值,默认没有显示出来。
.LFB0:
    pushq   %rbx            #保存%rbx寄存器到栈上 rbx rbp r12~r15为被调用者保存的寄存器
    movq    %rdx, %rbx      #移动%rdx到%rbx中
    call    mult2          #调用函数mult2
    movq    %rax, (%rbx)   #返回值存在在%rax中,然后存储到M[%rbx]等价于 *dst = t 
    popq    %rbx           #出栈,弹出压栈时保存到栈上的%rbx的值
    ret

在看一个例子我们编写main.c函数

#include<stdio.h>
void mulstore(long,long ,long*);
long mult2(long a,long b){
    long s = a * b;
    return s;
}
int main(){
    long d;
    multstore(2,3,&d);
    printf("2*3=%ld\n",d);
    return 0;
}
使用命令 gcc -Og -o prog main.c multstore.c,然后使用objdump -d prog 查看反汇编代码。
000000000040061b <multstore>:
  40061b:   53                      push   %rbx
  40061c:   48 89 d3                mov    %rdx,%rbx
  40061f:   e8 92 ff ff ff          callq  4005b6 <mult2> #mult2具体地址已经由链接器替换了。
  400624:   48 89 03                mov    %rax,(%rbx)
  400627:   5b                      pop    %rbx
  400628:   c3                      retq
  400629:   0f 1f 80 00 00 00 00    nopl   0x0(%rax)  #插入空指令 保证使得下一个函数地址按照16字节对齐。
而且prog可执行程序的反汇编后,得到的地址都是虚拟内存空间的地址,由此可见,在prog文件内,虚拟内存地址已经由链接器分配了。

数据格式以及访问信息

大多数gcc生成的汇编指令都带有一个后缀来表示操作数的大小,比如movb(传送字节),movw(传送字),movl(传送双字),movq(传送四字)。
x86-64的cpu包含一组16个存储64位值的通用寄存器,这些寄存器存储整数变量以及指针(浮点有专门的寄存器)。


43A24D25-B84A-4B7E-A21C-577F074711DE.png

指令可以对这16个寄存器的地位存储不同大小的数据。%rax作为函数返回值只用,%rdi,%rsi,%rdx以及%rcx作为函数的参数使用。%rsp栈针,这个指向运行时栈结束的位置。
这里说明下调用者保存以及被调用者保存的意思。比如过程P 调用Q。Q如果使用调用者保存的寄存器,必须在返回之前把寄存器的值还原,Q的做法是把寄存器压入栈中,然后结束后弹出栈(注意是:相反顺序,因为栈是先进后出)。

操作数指示符

大多数指令都有一个或者多个操作数。操作数的类型:

  1. 立即数(ATT汇编的写法,数字加一个,比如1024,表示十进制的1024立即数),汇编器会自动选择最紧凑的方法进行数值编码。浮点数就不能表示立即数,这个后面再介绍。
    2.寄存器。16个通用寄存器中的低1,2,4或者8字节的一个作为操作数,如果r_a表示寄存器那么 R[r_a]来引用他的值。
    3.内存引用。通常用M_b[ADDR]来书写表示内存引用的值。
    2.png

    其中比例因子s必须是1,2,4,8。比例变址寻址只能做有限的乘法。所以一些其他比例时,往往通过多条指令组合来形成。

数据传送指令

指令格式 MOV S D。表示把S传送入D。
MOV类指令,movb,movw,movl以及movq。分别表示传送不同长度的操作数。
x86-64加了一条限制,2个操作数都位内存的引用。所以当内存位置复制到另外一个内存位置时,只能先把内存位置引用的值先传送给一个寄存器,然后在从该寄存器传送至另外一个内存位置。
MOV指令正常只会更新目的操作数寄存器指定的字节或者内存位置。movl这个是例外,当以寄存器作为目的时,会把该寄存器的高4位置为0。
特别之处,movq和movabsq的区别,movq的源参数如果为立即数,只能是32位的补码,然后符号扩展到64位。movabsq可以的源参数可以为一个64位的立即数。

int main(){
    __asm__("movq $-0x80000000,%rax");
    __asm__("movq $0x80000000,%rax");
    __asm__("movabsq $0x80000000,%rax");
    return 0;
}
使用gdb反汇编后
(gdb) disas main
Dump of assembler code for function main:
   0x00000000004004d6 <+0>: push   %rbp
   0x00000000004004d7 <+1>: mov    %rsp,%rbp
   0x00000000004004da <+4>: mov    $0xffffffff80000000,%rax
   0x00000000004004e1 <+11>:    movabs $0x80000000,%rax
   0x00000000004004eb <+21>:    movabs $0x80000000,%rax
   0x00000000004004f5 <+31>:    mov    $0x0,%eax
   0x00000000004004fa <+36>:    pop    %rbp
   0x00000000004004fb <+37>:    retq

我们可以观察到当立即数可以用32位补码表示时,会拓展符号位到64位。
当无法用32位补码表示时,指令mov被转换成movabsq。
其中-0x80000000为Tmin的大小。而+0x80000000为Tmax + 1,超过32位补码表示范围。

有两类指令可以将较小的源值复制到较大的目的使用。
1.MOVZ类指令,进行零扩展,分别为movzbw,movzbl,movzbq,movzwl,movzwq.可以根据字面意思去理解,里面没有把双字拓展成四字的指令。(这个可以用movl来替代,movl自动会把高4字节置0)
2.MOVS类指令,进行符号扩展,分别为movsbw,movsbl,movsbq,movswl,movswq,movslq,以及ctlq。其中ctlq只能作用与寄存器%eax和%rax,是将%eax符号扩展成%rax。%ctlq等价于 movslq %eax,%rax。只是指令更紧凑。
MOVS以及MOVZ类扩展指令的目的都是一个寄存器。

压入和弹出栈数据

栈遵循着“先进后出”的规则。通过push操作把数据压入栈中,通过pop操作删除数据。x86中,栈是向下增长的,栈顶元素的地址是所有栈中元素最低的。栈指针%rsp保存着栈顶元素的地址。

  • pushq S 等价于 R[%rsp] \leftarrow R[%rsp] - 8; M[R[%rsp]] \leftarrow S ; //将双字压入栈
  • popq D 等价于 D \leftarrowM[R[%rsp]] ; R[%rsp] \leftarrow R[%rsp] + 8; //将双字弹出栈。
    push可以用subq和movq指令来替代。pushq指令编码只需要一个字节。而subq和movq指令需要更多的字节。
int main(){
    __asm__("movq %rbp,%rax");
    __asm__("subq %rbp,%rax");
    __asm__("pushq %rdi");
    __asm__("pushq %rsi");
    __asm__("popq %rsi");
    __asm__("popq %rdi");
    return 0;
}
写如上测试函数,查看反汇编的地址,可以看到mov和sub分别占用了3个字节,而push和pop指令分别只占用一个字节。
   0x00000000004004d7 <+1>: mov    %rsp,%rbp
   0x00000000004004da <+4>: mov    %rbp,%rax
   0x00000000004004dd <+7>: sub    %rbp,%rax
   0x00000000004004e0 <+10>:    push   %rdi
   0x00000000004004e1 <+11>:    push   %rsi
   0x00000000004004e2 <+12>:    pop    %rsi
   0x00000000004004e3 <+13>:    pop    %rdi
   0x00000000004004e4 <+14>:    mov    $0x0,%eax
   0x00000000004004e9 <+19>:    pop    %rbp
一个字节的push和pop如何编码的,其后还加了一个寄存器。我们查看下内存。
 (gdb) x /1bx 0x00000000004004e0
0x4004e0 <main+10>: 0x57
(gdb) x /1bx 0x00000000004004e1
0x4004e1 <main+11>: 0x56
(gdb) x /1bx 0x00000000004004e2
0x4004e2 <main+12>: 0x5e
(gdb) x /1bx 0x00000000004004e3
0x4004e3 <main+13>: 0x5f
我们可以观察到指令编码中已经把寄存器的“编号”编码进去了。一个字节就能实现我们6个字节才能实现的功能。

算术和逻辑操作。

这些操作分为4组:加载有效地址,一元操作,二元操作以及移位。

加载有效地址

leaq本质是mov指令的一个变种,只是名字太迷惑了,实际上根本没有引用内存。如果%rdx的值为x,那么leaq 7(%rdx,rdx,4) ,%rax 等价于 %rax \leftarrow 7+5x。类似与比例变址寻址,leaq指令也只能做有限的乘法。leaq指令可以间接的描述一些算数运算。

int fun(long x){
    long *px = &x;
    return 0;
}
查看汇编结果
0000000000400546 <fun>:
  400546:   55                      push   %rbp
  400547:   48 89 e5                mov    %rsp,%rbp
  40054a:   48 89 7d e8             mov    %rdi,-0x18(%rbp)
  40054e:   48 8d 45 e8             lea    -0x18(%rbp),%rax
  400552:   48 89 45 f8             mov    %rax,-0x8(%rbp)
  400556:   b8 00 00 00 00          mov    $0x0,%eax
  40055b:   5d                      pop    %rbp
  40055c:   c3                      retq
其中%rdi表示参数x。leaq本质上还是一个做一个算数运算。只是和栈指针进行减法运算得到栈上存储x的地址,然后把地址传送到%rax寄存器。

一元和二元操作

一元操作(++,--)对应指令INC和DEC,以及取负取反对应指令(NEG和NOT)。参数只有一个,所以参数即是源又是目标。
二元操作,类似y+=x,第二个操作数即是源又是目标。指令%subq %rax,%rdx表达从%rdx中减去%rax存储的值。这些操作没啥好说的。

移位操作

移位操作,左移都是最低位都是填充0.右移分为算术右移和逻辑右移。算术右移对应的是补码,逻辑右移对应的是无符号数。所以对应的汇编指令中也是不同的指令了。

  • 左移指令 SAL和SHL分别为算术左移和逻辑左移,都是一样,都是最低位填充0.
  • 右移指令 SAR和SAR分别为算术右移和逻辑左移,逻辑左移高位填充0,算术右移高位填充符号位。

特殊的算术操作

两个64位数的乘法,不论是补码的形式还是无符号的,乘法的结果需要用128位来表示。

  • 针对乘法而言,由于没有128位的通用寄存器,所以用两个寄存器%rax %rdx来存储。
    imulq R[%rdx]:R[%rax] \longleftarrow S\timesR[%rax] 有符号乘法
    mulq R[%rdx]:R[%rax] \longleftarrow S\timesR[%rax] 无符号乘法
#include<inttypes.h>
typedef unsigned __int128 uint128_t;
void fun(unsigned long x,unsigned long y,uint128_t* p){
    *p = x * (uint128_t) y;
} 
  反汇编后得到的结果
   0x00000000004004f0 <+0>: mov    %rdi,%rax
   0x00000000004004f3 <+3>: mov    %rdx,%r8
   0x00000000004004f6 <+6>: mul    %rsi
   0x00000000004004f9 <+9>: mov    %rax,(%r8)
   0x00000000004004fc <+12>:    mov    %rdx,0x8(%r8)
   0x0000000000400500 <+16>:    retq
%rdi存储x,%rsi存储y,%rdx存储p。由于是小端所以%rax结果应该放在低内存,%rdx内容应该放在高内存。
特别注意的时,得到这个结果使用了gcc编译时使用-O2的优化,如果使用O0的话,表达式 (uint128_t) y)这个就会展开,生成一个临时的%rdx:%rax的组合表达128位寄存器。

如果没有定义为128位类型,那么乘法并没有用到%rdx:%rax的组合,针对溢出的部分,直接丢失。

#include<stdio.h>
int fun(unsigned long arg,unsigned long *p){
    unsigned long x = 0x8000000000000001u;
    unsigned long y = x *arg;
    *p = y;
    printf("y=%llx\n",y);
    return 0;
}
int main(){
    unsigned long a;
    fun(2u,&a);
    return 0;
}
得到结果为2,这里并没有直接写 0x8000000000000001u*2u,而是用一个函数调用的方式就是为了避免编译器会用移位来替代乘法指令。
  • 针对除法,有符号除法指令idiv将%rdx:%rax作为一个被除数,而除数由操作数给出。结果是将商存在寄存器%rax中,余数存储在%rdx中。大多数除法应用是被除数往往64位就够了,所以%rax存储被除数,%rdx设置全0(无符号数)或者%rax的符号位(补码)。继续实验来证明。
int div(long x,long y,long*qp,long*rp){
    long q = x/y;
    long r = x%y;
    *qp = q;
    *rp = r;
}
同样基于O2的优化,产生的汇编代码。%rdi存储x,%rsi存储y,%rdx存储qp,%rcx存储rp.
   0x0000000000400520 <+0>: mov    %rdi,%rax #保存x到%rax
   0x0000000000400523 <+3>: mov    %rdx,%rdi #保存qp到%rdi
   0x0000000000400526 <+6>: cqto    #拓展%rax到%rdx:%rax,即符号位拓展。
   0x0000000000400528 <+8>: idiv   %rsi #用%rdx:%rax里的值 除%rsi里的值(y) 
   0x000000000040052b <+11>:    mov    %rax,(%rdi)  #把商存储到*qp
   0x000000000040052e <+14>:    mov    %rdx,(%rcx)#把余数存储到*qp
#cqto R[%rdx]:R[%rax]$\longleftarrow$符号拓展R[%rax].
#针对无符号的除法时,要把%rdx全部设置成0.所以可以用异或指令xor    %edx,%edx,反汇编出来后也是这么做的。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 215,133评论 6 497
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,682评论 3 390
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,784评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,508评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,603评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,607评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,604评论 3 415
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,359评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,805评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,121评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,280评论 1 344
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,959评论 5 339
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,588评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,206评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,442评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,193评论 2 367
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,144评论 2 352

推荐阅读更多精彩内容