X86-64和ARM64用户栈的结构 (1) ---背景介绍

背景

主要基于Linux,介绍X86-64和ARM64的用户栈结构。断断续续的学了不少和栈相关的知识,今天打算整理用户栈相关的知识,废话少说,下面进入正题。python

栈的定义和类别

栈有时也称堆栈,是一种受限的线性表,只能在线性表的一端按序进行插入(进栈)和删除(出栈),所以先进栈的数据会后出栈。为了便于描述,咱们习惯将在线性表进行插入和删除的一端称为栈顶,另外一端称为栈底。栈顶会随着插入和删除而发生变化,栈底则保持不变。ide

其实,栈在计算机中就是一块连续的存储区域(至少虚拟地址是连续的),只不过在这块连续的存储区域写入和删除数据按照先进后出的规则进行,在计算机中使用两个指针就能够彻底描述一个栈,bp(base pointer)指向栈底,sp(stack pointer)指向栈顶,以下图所示。
X86-64和ARM64用户栈的结构 (1) ---背景介绍函数

上面主要讲了栈的定义,在上面栈的定义中至少有两个地方没有说清楚,一是往栈中增长数据时,栈是往高地址增长仍是往低地址增长;二是栈顶指针SP指向的地方是否存放数据。向高地址增加的栈称为递增栈(Ascendant Stack),向低地址增加的栈称为递减栈(Decendant Stack)。SP指向栈顶元素(即SP指向的地方存放数据)的栈为满栈(Full Stack),SP指向下一个栈顶元素位置(即SP指向的地方不存放数据)的栈为空栈(Empty Stack)。很显然一个栈不能同时为递增栈和递减栈,也不能同时为满栈和空栈。所以,存在4种类型的栈,即空增栈(Empty Ascendant Stack,EA)、空减栈(Empty Descendant Stack,ED)、满增栈(Full Ascendant Stack,FA)和满减栈(Full Descendant Stack,FD)。设计

空增栈(Empty Ascendant Stack,EA)

在对空增栈中压入数据时,先把数据放入SP所指的位置处,而后SP=SP+1。对这种栈的压入操做,至关于C语言的 memory[SP++]=data;或者至关于ARM64的汇编语言str x1,[SP],#8。出栈操做至关于C语言的data=memory[--SP];或者ARM64的汇编语言ldr x1,[SP,#-8]!
X86-64和ARM64用户栈的结构 (1) ---背景介绍指针

空减栈(Empty Descendant Stack,ED)

在对空减栈中压入数据时,先把数据放入SP所指的位置处,而后SP=SP-1。对这种栈的压入操做,至关于C语言的 memory[SP--]=data;或者至关于ARM64的汇编语言str x1,[SP],#-8。出栈操做至关于C语言的data=memory[++SP];或者ARM64的汇编语言ldr x1,[SP,#8]!
X86-64和ARM64用户栈的结构 (1) ---背景介绍code

满增栈(Full Ascendant Stack,FA)

在对满增栈中压入数据时,先对SP操做腾出位置SP=SP+1,而后数据放入SP指向的位置。对这种栈的压入操做,至关于C语言的 memory[++SP]=data;或者至关于ARM64的汇编语言str x1,[SP,#8]!。出栈操做至关于C语言的data=memory[SP--];或者ARM64的汇编语言ldr x1,[SP],#-8
X86-64和ARM64用户栈的结构 (1) ---背景介绍生命周期

满减栈(Full Descendant Stack,FD)

在对满减栈中压入数据时,先对SP操做腾出位置SP=SP-1,而后数据放入SP指向的位置。对这种栈的压入操做,至关于C语言的 memory[--SP]=data;或者至关于ARM64的汇编语言str x1,[SP,#-8]!。出栈操做至关于C语言的data=memory[SP++];或者ARM64的汇编语言ldr x1,[SP],#8。或者X86-64的汇编指令push r1x86-64的汇编指令push和pop操做栈是按照满减栈的规则进行。默认状况下,ARM64也使用满减栈的规则操做栈。
X86-64和ARM64用户栈的结构 (1) ---背景介绍进程

栈的生命周期

栈的生命周期是和进程的生命周期保持一致的,进程在则栈在,进程亡则栈亡。所以,不妨从进程的生命周期探讨栈的生命周期。一个用户进程从无到开始运行,须要通过几个重要的步骤:内存

  • Linux首先建立一个task_struct用于管理进程的方方面面。这里只是有了进程的“草图”,进程尚未被建立。
  • 创建进程的虚拟地址空间,也即创建页表,创建虚拟地址到物理地址的映射,到这时一个用户进程所需的基本元素已经具有,一个进程被建立完成,在建立进程的过程当中,进程的内核栈也被建立,内核栈不在本文的说明范围内。
  • 接下来就须要可执行文件自己的参与,读取可执行文件头,解析文件头,文件头的前几个字节会指出当前文件是何种类型,若是是#!/bin/sh或 #!/bin/python 则该文件是脚本文件,有负责脚本文件的加载程序,本文只关注可执行文件。创建虚拟地址和可执行文件之间的映射。
  • 初始化进程环境,其中比较重要的一项即是初始化用户栈
  • 跳转到可执行文件的入口,执行可执行文件,运行到用户程序main函数,这其中主要右libc对进行管理。
  • main()函数经过切换栈帧调用其它子函数,子函数也能经过切换栈帧调用其子函数。
  • mian()函数返回,整个进程结束,释放栈占的内存,栈消失

结合上面所述以及下图所示,栈的生命周期能够分为4个部分:编译器

  • Linux Kernel建立用户栈,为栈分配内存空间,处理传递给用户的参数,将参数压入栈中,压入指向参数的argv,计算出argc并将其压栈。
  • libc的_start函数将 Linux Kernel建立的栈和libc库函数接上头,由体系结构相关的汇编语言编写,核心做用是将栈顶地址赋值给SP,还将Linux设置的栈传递、参数传递以及一些库函数的函数指针传递给C语言编写的函数__libc_start_mian_start函数只是起到一个过渡做用,根据CPU的体系结构将Linux Kernel初始化好的栈传递给后续的C语言编写的函数。
  • libc的__libc_start_mian函数是一个C语言写的函数,运行到该函数时用户栈的结构已是编译器设计的了,同时因为_start函数已经设置好了SP的值,各类压栈、出找操做都在不断调整SP的值。该函数的功能主要有,main调用前的初始化工做;调用main;main函数返回后的清尾工做。
  • 编译器设计main函数及其调用的子函数的栈。
    X86-64和ARM64用户栈的结构 (1) ---背景介绍

    用户栈在系统中的位置

    对于Linux内核而言,将整个内存空间划分为两个部分,Kernel Space 和User Space,前者用于支撑Linux Kenrel自己的运行所需空间,后者就是用于支持用户程序所需的运行空间。用户栈就是位于用户空间,通常位于用户空间的最高部分,向低地址处增加。
    X86-64和ARM64用户栈的结构 (1) ---背景介绍

相关文章
相关标签/搜索