IT狗工作室C语言C语言&嵌入式

第6篇-戏说程序栈 x86_64过程调用

2019-10-26  本文已影响0人  铁甲万能狗

这是程序栈话题的最后一篇,可能有人会问,你前面5篇写那么多x86程序栈的文章干什么?请耐心看下去,即便现在x64硬件流行的今天,x86的过程调用约定仍然有存在的现实意义,这个戏说程序栈的最终篇,我们探讨以下x86程序栈和x86_64的差异性。

x86 vs x86_64 过程调用约定

下表是对x86寄存器和x86_64寄存器的一个使用约定的对比表,从该表可知,由于x86_64中的物理寄存器数量比x86的多出了一倍,因此可以在很大程度上减轻了程序调用对栈的依赖,并且更好地利用寄存器交换临时数据,例如我们可以将参数直接缓存到寄存器中,并且也可以将局部变量直接缓存到寄存器,当然我们会耗尽所有可用的寄存器,然后又回到x86程序栈的约定的操作方式:参数先入栈,需要调用该参数时又加载到寄存器中。但是多数情况下,我们仍然尽最大努力尝试绕过函数栈直接利用寄存器,并避免过多地使用栈内存写入

ss6.png

回到我们上表,16个通用的寄存器,分别是8字节的位宽,我们知道

红色区域

%rsp所指向的位置之外的128字节区域被视为已保留,并且不得由信号或中断处理程序修改。 因此,函数可以使用此区域存储跨函数调用不需要的临时数据。 尤其是,叶子函数(就是位于整个函数调用链的末端的函数)可以在整个堆栈框架中使用此区域,而不是在序言和结语中调整栈指针。 这个地区被称为“红色区域(Red Zone)”

需要注意的是,红色区域会被函数调用所破坏,因此通常在叶子函数(不调用其他函数的函数)中使用。

入门示例导入

我之前在《x86/x86_64汇编的常用指令的比较》中已经用了一个x86_64的程序栈调用示例,这里不再复述。

x86_64栈帧

通常情况下,x86-64函数不在需要栈帧,唯一写入栈的操作就是执行到callq指令时候的“返回地址”8个字节。这使得栈帧的结构变得非常简单。那么什么情况下函数需要栈帧呢?

复杂的x86_64示例分析

long div(long x,long y,long z){

   long s=(x+y)/z;
   return s;
}

long calc(long x, long y, long z, long p,
            long q, long s, long u, long v){
    long m = x * y * z /p + q * s * u / v;
    long n = (x + y + z + p)/(q - s + u * v);
    long L = div(m, n, m % n);
    return L + 20;
}

int main(void){
   long s=calc(10,15,16,23,24,32,51,31);
   return 0;
}

本示例使用如下指令执行反编译,使用 -O1的优化选项
gcc -S x64stack.c -O1 -o x64stack.s -fno-asynchronous-unwind-tables -fno-stack-protector

仅仅是calc函数得到汇编代码有40行之多,所以这里不打算贴出来了 这里主要x86_64栈的调用过程

第一,下图调用main函数执行callq指令后,并且main函数已经传递给calc的8个参数发生如下细节:

第二,在calc函数中,编译器会首先处理“long m=x*y*z/p+q*s*u/v;”这条语句的前半部分,如下图红色所示,转换为如下等价的指令集。根据我们前面文章提过,imul指令会计算后的结果存储到目标操作数RAX寄存器当中。

第三,同理,编译会处理“long m=x*y*z/p+q*s*u/v;”后半部分的表达式“q*s*u/v”,等价的语句请看如下图红色的汇编指令,这里需要注意的是,

接下来执行“add %rax,%10”“这条指令并且会将前面两个步骤计算的计算的结果RAX寄存器和R10相加,并且计算结果会重新修改R10寄存器的值为1367,这是局部变量m的值,这是遵循x86_64局部变量的使用约定

同理,编译器处理处理的表达式“long n=(x+y+z+p)/(q-s+u*v)”会等价剩余的指令集合,此时的RCX缓存的计算结果为0,到目前为止,calc函数目前执行的任何数学运算没有执行执行任何calc栈帧的入栈和出栈的操作,这说明了

addq    %rsi, %rdi
addq    %rbp, %rdi
addq    %rdi, %rcx
subq    %r9, %r8
imulq   %rbx, %r11
addq    %r11, %r8
movq    %rcx, %rax
cqto
idivq   %r8
movq    %rax, %rcx

执行到long n=(x+y+z+p)/(q-s+u*v);这条语句时,我们局部变量m,n的值是缓存到rcx中,更深一层地说,到目前为止,calc函数中的局部变量并没有缓存到“红色区域”,我们从本文最后贴出的完整汇编代码你也没有找到类似“mov %rax -N(%rsp)” 其中N>0这样的表达式,因此可以引证本文的例子没有使用到红色区域来存储局部变量的状态,而是优先使用寄存器来缓存局部变量。

其实接下来,步骤我们就没必要往下说,这里只是贴出一些关键的步骤,到这里读者可能会问,为什么calc没有调用div函数,你应该记得我们本示例编译的时候使用的是-O1的优化选项,在调用函数中,编译器会将被调用函数div内部的简单表达式语句,替换掉调用函数中的 div(m, n, m %(n+1))语句,即被替换的伪代码如下:
long L=(m+n)/(m%(n+1))
这类似于C++中的inline函数得到的编译效果,这样有助于避免了函数调用过程中的产生额外函数栈的时间消耗,下面附上完整的代码,从下面的两点证实了这个说法。

总结:

后记:

我原本打算在本文示例中使用double类似作为calc的参数的,但为了简化对栈话题的阐述,大部分写栈话题的软文写手,都会绕开浮点数作为示例(包括我),如果你对浮点数的内存表示有了解过的话,浮点数的入栈操作和数据处理,跟其他基本数据类型是不一样的,甚至有特定浮点数寄存器用于处理它们。在汇编语法中,浮点数有自己一套的特殊指令.如果你打算深入了解类似C中的结构体(或C++的类)在栈内存中是如何分布的,浮点数在汇编层面的操作是个绕不过去的坎,因为我们平时用类来封装算法实现,经常需要使用到浮点数。

所以关于浮点数的内存管理的话题,我有空会再补上,因为光浮点数,足以分开3-4编文章来阐述它。

上一篇下一篇

猜你喜欢

热点阅读