文章目录
一、x86-64来源历史
1)说到x86-64,总不免要说说AMD的牛逼,x86-64是x86系列中集大成者,继承了向后兼容的优良传统,
最早由AMD公司提出,代号AMD64;正是由于能向后兼容,AMD公司打了一场漂亮翻身战。导致Intel不
得不转而生产兼容AMD64的CPU。这是IT行业以弱胜强的经典战役。不过,大家为了名称延续性,更习
惯称这种系统结构为x86-64
2)X86-64开创了编译器的新纪元,在之前的时代里,Intel CPU的晶体管数量一直以摩尔
定律在指数发展,各种新奇功能层出不穷,比如:条件数据传送指令cmovg,SSE指令等。但是GCC只能
保守地假设目标机器的CPU是1985年的i386,额。。。这样编译出来的代码效率可想而知,虽然GCC额
外提供了大量优化选项,但是这对应用程序开发者提出了很高的要求,会者寥寥。X86-64的出现,给
GCC提供了一个绝好的机会,在新的x86-64机器上,放弃保守的假设,进而充分利用x86-64的各种特
性,比如:在过程调用中,通过寄存器来传递参数,而不是传统的堆栈。又如:尽量使用条件传送指
令,而不是控制跳转指令
二、x86-64的两种工作模式
1)模式一
32位OS既可以跑在传统模式中,把CPU当成i386来用
2)模式二
又可以跑在64位的兼容模式中,更加神奇的是,可以在32位的OS上跑64位的应用程序
三、寄存器介绍
- 前提
先明确一点,本文关注的是通用寄存器(后简称寄存器)。既然是通用的,使用并没有限制;后面介
绍寄存器使用规则或者惯例,只是GCC(G++)遵守的规则。因为我们想对GCC编译的C(C++)程序进行分
析,所以了解这些规则就很有帮助
1)64位相对于32位做的让步和x86-64寄存器的介绍
- 补充
寄存器集成在CPU上,存取速度比存储器快好几个数量级,寄存器多了,GCC就可以更多的
使用寄存器,替换之前的存储器堆栈使用,从而大大提升性能。
- 具体让步和介绍
1)X86-64中,所有寄存器都是64位,相对32位的x86来说,标识符发生了变化,比如:从原来的%ebp变成
了%rbp。为了向后兼容性,%ebp依然可以使用,不过指向了%rbp的低32位
2)X86-64寄存器的变化,不仅体现在位数上,更加体现在寄存器数量上。新增加寄存器%r8到%r15。加上
x86的原有8个,一共16个寄存器
3)X86-64有16个64位寄存器,分
别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,
%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15
2)具体寄存器的功能
1)%rax 作为函数返回值使用
2)%rsp 栈指针寄存器,指向栈顶
3)%edi,%esi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。
4)%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便
用,调用子函数之前要备份它,以防他被修改
5)%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值,用作中介值
四、栈帧
1)栈帧结构
- 定义:
在对应机器语言中,GCC把过程转化成栈帧(frame),简单的说,每个
栈帧对应一个过程。X86-32典型栈帧结构中,由%ebp指向栈帧开始,%esp指向栈顶
2)函数进入和返回
函数的进入和退出,通过指令call和ret来完成,给一个例子
- 代码
#include <stdlib.h>
#include <stdio.h>
int foo(int x)
{
int array[] ={1,3,5};
return array[x];
}
int main(int argc,char*argv[])
{
int i = 1;
int j = foo(i);
fprintf(stdout,"i=%d,j=%d\n",i,j);
return 0;
}
gcc -S -o show.s show.c
.file "show.c"
.text
.globl foo
.type foo, @function
foo:
.LFB5:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $48, %rsp
movl %edi, -36(%rbp)
movq %fs:40, %rax
movq %rax, -8(%rbp)
xorl %eax, %eax
movl $1, -20(%rbp)
movl $3, -16(%rbp)
movl $5, -12(%rbp)
movl -36(%rbp), %eax
cltq
movl -20(%rbp,%rax,4), %eax
movq -8(%rbp), %rdx
xorq %fs:40, %rdx
je .L3
call __stack_chk_fail@PLT
.L3:
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE5:
.size foo, .-foo
.section .rodata
.LC0:
.string "i=%d,j=%d\n"
.text
.globl main
.type main, @function
main:
.LFB6:
.cfi_startproc
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset 6, -16
movq %rsp, %rbp
.cfi_def_cfa_register 6
subq $32, %rsp
movl %edi, -20(%rbp)
movq %rsi, -32(%rbp)
movl $1, -8(%rbp)
movl -8(%rbp), %eax
movl %eax, %edi
call foo
movl %eax, -4(%rbp)
movq stdout(%rip), %rax
movl -4(%rbp), %ecx
movl -8(%rbp), %edx
leaq .LC0(%rip), %rsi
movq %rax, %rdi
movl $0, %eax
call fprintf@PLT
movl $0, %eax
leave
.cfi_def_cfa 7, 8
ret
.cfi_endproc
.LFE6:
.size main, .-main
.ident "GCC: (Ubuntu 7.5.0-3ubuntu1~18.04) 7.5.0"
.section .note.GNU-stack,"",@progbits
- 注释
1)main函数第55行指令 call foo做了两件事
pushq %rbp //保存下一条指令(main函数第56行的代码地址)的地址,用于函数返回继续执行
je .L3 //跳到3级CPU缓存
2)Foo函数跳到L3的ret指令相当于
pop;l %rbp //恢复指针寄存器
- 补充
1)push和pushl的区别:
汇编器可以自己判断自己要操作的是什么长度的操作对象;但是当汇编器不能自己判断操作对象长度时,就需要使用pushl之类的指令来指明操作对象长度;
2)je就是跳转
je 是条件转移 (jump) 指令,转移条件是此前的两数比较结果是相等(equal)
3)三级缓存(L1、L2、L3)是什么?
以近代CPU的视角来说,三级缓存(包括L1一级缓存、L2二级缓存、L3三级缓存)都是集成在CPU内
的缓存,它们的作用都是作为CPU与主内存之间的高速数据缓冲区,L1最靠近CPU核心;L2其次;L3
再次。运行速度方面:L1最快、L2次快、L3最慢;容量大小方面:L1最小、L2较大、L3最大。CPU会
先在最快的L1中寻找需要的数据,找不到再去找次快的L2,还找不到再去找L3,L3都没有那就只能去内
存找了