x86_64 Linux 运行时栈的字节对齐
前言
C语言的过程调用机制(即函数之间的调用)的一个关键特性(起始大多数编程语言也是如此)都是使用了栈数据结构提供的后进先出的内存管理原则。每一个函数的栈空间被称为栈帧,一个栈帧上包含了保存的寄存器、分配给局部变量的空间以及传递给要调用函数的参数等等。一个基本的栈结构如下图所示:

但是,有一点需要引起注意的是,过程调用的参数是通过栈来传递的,并且分配的局部变量也在栈上,那么对于不同字节长度的参数或变量,是如何在栈上为它们分配空间的?这里所涉及的就是我们要探讨的字节对齐。
本文示例用到的环境如下:
- Ubuntu x86_64 GNU/Linux
- gcc 7.4.0
数据对齐
许多计算机系统对基本数据类型的合法地址做了一些限制,要求某种类型对象的地址必须是某个值K的倍数,其中K具体如下图。这种对齐限制简化了形成处理器和内存系统之间接口的硬件设计。举个实际的例子:比如我们在内存中读取一个8字节长度的变量,那么这个变量所在的地址必须是8的倍数。如果这个变量所在的地址是8的倍数,那么就可以通过一次内存操作完成该变量的读取。倘若这个变量所在的地址并不是8的倍数,那么可能就需要执行两次内存读取,因为该变量被放在两个8字节的内存块中了。
| K | 类型 |
|---|---|
| 1 | char |
| 2 | short |
| 4 | int, float |
| 8 | long,double,char* |
无论数据是否对齐,x86_64硬件都能正常工作,但是却会降低系统的性能,所以我们的编译器在编译时一般会为我们实施数据对齐。
栈的字节对齐
栈的字节对齐,实际是指栈顶指针必须须是16字节的整数倍。栈对齐帮助在尽可能少的内存访问周期内读取数据,不对齐堆栈指针可能导致严重的性能下降。
上文我们说,即使数据没有对齐,我们的程序也是可以执行的,只是效率有点低而已,但是某些型号的Intel和AMD处理器对于有些实现多媒体操作的SSE指令,如果数据没有对齐的话,就无法正确执行。这些指令对16字节内存进行操作,在SSE单元和内存之间传送数据的指令要求内存地址必须是16的倍数。
因此,任何针对x86_64处理器的编译器和运行时系统都必须保证分配用来保存可能会被SSE寄存器读或写的数据结构的内存,都必须是16字节对齐的,这就形成了一种标准:
- 任何内存分配函数(alloca, malloc, calloc或realloc)生成的块起始地址都必须是16的倍数。
- 大多数函数的栈帧的边界都必须是16直接的倍数。
如上,在运行时栈中,不仅传递的参数和局部变量要满足字节对齐,我们的栈指针(%rsp)也必须是16的倍数。
三个示例
我们用三个实际的例子来看一看为了实现数据对齐和栈字节对齐,栈空间的分配具体是怎样的。
如下是CSAPP上的一个示例程序。
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char *a4p) {
*a1p += a1;
*a2p += a2;
*a3p += a3;
*a4p += a4;
}
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, x4);
return (x1+x2)*(x3+x4);
}
使用如下命令进行编译和反编译:
$ gcc -Og -fno-stack-protector -c call_proc.c
$ objdump -d call_proc.o
其中-fno-stack-protector参数指示编译器不添加栈保护者机制
生成的汇编代码如下,这里我们仅看call_proc()中的栈空间分配
0000000000000015 <call_proc>:
15: 48 83 ec 10 sub $0x10,%rsp
19: 48 c7 44 24 08 01 00 movq $0x1,0x8(%rsp)
20: 00 00
22: c7 44 24 04 02 00 00 movl $0x2,0x4(%rsp)
29: 00
2a: 66 c7 44 24 02 03 00 movw $0x3,0x2(%rsp)
31: c6 44 24 01 04 movb $0x4,0x1(%rsp)
36: 48 8d 4c 24 04 lea 0x4(%rsp),%rcx
3b: 48 8d 74 24 08 lea 0x8(%rsp),%rsi
40: 48 8d 44 24 01 lea 0x1(%rsp),%rax
45: 50 push %rax
46: 6a 04 pushq $0x4
48: 4c 8d 4c 24 12 lea 0x12(%rsp),%r9
4d: 41 b8 03 00 00 00 mov $0x3,%r8d
53: ba 02 00 00 00 mov $0x2,%edx
58: bf 01 00 00 00 mov $0x1,%edi
5d: e8 00 00 00 00 callq 62 <call_proc+0x4d>
...
15行(我们具体以代码中给出的行号,其实这些数字应该是指令的起始位置,姑且就这样叫吧)中先将%rsp减去0x10,为4个局部变量共分配了16个字节的空间,并且在45和46行,程序将%rax和$0x4入栈,联系该函数的C语言程序和汇编程序中的具体操作,不难知,栈上的具体空间分配如下图所示:

图中,为了使栈字节对齐,4单独占用了一个8字节的空间,并且栈中的每一个类型的变量,都符合数据对齐的要求。
如果我们的参数8占用的字节数减少,会不会减少栈空间的占用呢?我们将上面的C语言程序的稍微改一改,如下:
void proc(long a1, long *a1p,
int a2, int *a2p,
short a3, short *a3p,
char a4, char a5) { // char *a4p改为了char a5
*a1p += a1;
*a2p += a2;
*a3p += a3;
a5 += a4;
}
long call_proc()
{
long x1 = 1; int x2 = 2;
short x3 = 3; char x4 = 4;
proc(x1, &x1, x2, &x2, x3, &x3, x4, x4); // 相应的改变了最后一个参数
return (x1+x2)*(x3+x4);
}
call_proc()的汇编如下:
000000000000000a <call_proc>:
a: 48 83 ec 10 sub $0x10,%rsp
e: 48 c7 44 24 08 01 00 movq $0x1,0x8(%rsp)
15: 00 00
17: c7 44 24 04 02 00 00 movl $0x2,0x4(%rsp)
1e: 00
1f: 66 c7 44 24 02 03 00 movw $0x3,0x2(%rsp)
26: 48 8d 4c 24 04 lea 0x4(%rsp),%rcx
2b: 48 8d 74 24 08 lea 0x8(%rsp),%rsi
30: 6a 04 pushq $0x4
32: 6a 04 pushq $0x4
34: 4c 8d 4c 24 12 lea 0x12(%rsp),%r9
39: 41 b8 03 00 00 00 mov $0x3,%r8d
3f: ba 02 00 00 00 mov $0x2,%edx
44: bf 01 00 00 00 mov $0x1,%edi
49: e8 00 00 00 00 callq 4e <call_proc+0x44>
...
对照程序,栈的空间结构编程的如下如所示:

我们发现,栈空间的占用并没有减少,为了能够达到栈字节对齐的目的,参数8和参数7各占一个8字节的空间,该过程调用浪费了1 + 7 + 7 = 15字节的空间。但为了兼容性和效率,这是值得的。
我们再看另一个程序,当我们在栈中分配字符串时又是怎样的呢?
void function(int a, int b, int c) {
char buffer1[5];
char buffer2[10];
strcpy(buffer2, buffer1);
}
void main() {
function(1,2,3);
使用gcc -fno-stack-protector -o foo foo.c和objdump -d foo进行编译和反编译后,function()的汇编代码如下:
000000000000064a <function>:
64a: 55 push %rbp
64b: 48 89 e5 mov %rsp,%rbp
64e: 48 83 ec 20 sub $0x20,%rsp
652: 89 7d ec mov %edi,-0x14(%rbp)
655: 89 75 e8 mov %esi,-0x18(%rbp)
658: 89 55 e4 mov %edx,-0x1c(%rbp)
65b: 48 8d 55 fb lea -0x5(%rbp),%rdx
65f: 48 8d 45 f1 lea -0xf(%rbp),%rax
663: 48 89 d6 mov %rdx,%rsi
666: 48 89 c7 mov %rax,%rdi
669: e8 b2 fe ff ff callq 520 <strcpy@plt>
66e: 90 nop
66f: c9 leaveq
670: c3 retq
该过程共在栈上分配了32个字节的空间,其中包括两个字符串的空间和三个函数的参数的空间,这里需要提一下的是,尽管再x64下,函数的前6个参数直接用寄存器进行传递,但是有时候程序需要用到参数的地址,这个时候程序就不的不在栈上为参数分配内存并将参数拷贝到内存上,来满足程序对参数地址的操作。
联系程序,该过程的栈结构如下:

图中,因为char类型的地址可以从任意地址开始(地址为1的倍数),所以buffer1和buffer2是连续分配的,而三个int型变量则分配在了两个单独的8字节空间中。
小结
以上,我们看到,为了满足数据对齐和栈字节对齐的要求,或者说规范,编译器不惜牺牲了部分内存,这使得程序提高了兼容性,也提高了程序的性能。
完
参考:
- 《深入理解计算机系统》
- C函数调用过程解析(x86-64)
x86_64 Linux 运行时栈的字节对齐的更多相关文章
- Java虚拟机运行时栈帧结构--《深入理解Java虚拟机》学习笔记及个人理解(二)
Java虚拟机运行时栈帧结构(周志明书上P237页) 栈帧是什么? 栈帧是一种数据结构,用于虚拟机进行方法的调用和执行. 栈帧是虚拟机栈的栈元素,也就是入栈和出栈的一个单元. 2018.1.2更新(在 ...
- 深入理解java虚拟机(十) Java 虚拟机运行时栈帧结构
运行时栈帧结构 栈帧(Stack Frame) 是用于虚拟机执行时方法调用和方法执行时的数据结构,它是虚拟栈数据区的组成元素.每一个方法从调用到方法返回都对应着一个栈帧入栈出栈的过程. 每一个栈帧在编 ...
- 计算机是如何计算的、运行时栈帧分析(神奇i++续)
关于i++的疑问 通过JVM javap -c 查看字节码执行步骤了解了i++之后,衍生了一个问题: int num1=50; num1++*2执行的是imul(将栈顶两int类型数相乘,结果入栈), ...
- java虚拟机规范-运行时栈帧
前言 java虚拟机是java跨平台的基石,本文的描述以jdk7.0为准,其他版本可能会有一些微调. 引用 java虚拟机规范 java虚拟机规范-运行时数据区 java内存运行时的栈帧结构 java ...
- 【转载】深入理解Java虚拟机笔记---运行时栈帧结构
栈帧(Stack Frame)是用于支持虚拟机进行方法调用和方法执行的数据结构,它是虚拟机运行时数据区的虚拟机栈(Virtual Machine Stack)的栈元素.栈帧存储了方法的局部变量表,操作 ...
- linux 运行时加载不上动态库 解决方法(转)
1. 连接和运行时库文件搜索路径到设置 库文件在连接(静态库和共享库)和运行(仅限于使用共享库的程序)时被使用,其搜索路径是在系统中进行设置的.一般 Linux 系统把 /lib 和 /usr ...
- cortexm内核 栈的8字节对齐及关键字PRESERVE8
一.什么是栈对齐? 栈的字节对齐,实际是指栈顶指针须是某字节的整数倍.因此下边对系统栈与MSP,任务栈与PSP,栈对齐与SP对齐 这三对概念不做区分.另外下文提到编译器的时候,实际上是对编译器汇编器连 ...
- Intermediate_JVM 20180306 : 运行时数据区域
Java比起C++一个很大的进步就在于Java不用再手动控制指针的delete与free,统一交由JVM管理,但也正因为如此,一旦出现内存溢出异常,不了解JVM,那么排查问题将会变成一项艰难的工作. ...
- Java运行时内存划分与垃圾回收--以及类加载机制基础
----JVM运行时内存划分----不同的区域存储的内容不同,职责因为不同1.方法区:被线程共享,存储被JVM加载的类的信息,常量,静态变量等2.运行时常量池:属于方法区的一部分,存放编译时期产生的字 ...
随机推荐
- 激活函数、正向传播、反向传播及softmax分类器,一篇就够了!
1. 深度学习有哪些应用 图像:图像识别.物体识别.图片美化.图片修复.目标检测. 自然语言处理:机器创作.个性化推荐.文本分类.翻译.自动纠错.情感分析. 数值预测.量化交易 2. 什么是神经网络 ...
- Linux内核实战(二)- 操作系统概览
不知道你有没有产生过这些疑问: 桌面上的图标到底是啥?凭啥我在鼠标上一双击,就会出来一些不可描述的画面?都是从哪里跑出来的? 凭什么我在键盘上噼里啪啦地敲,某个位置就会显示我想要的那些字符? 电脑怎么 ...
- oracle 删除用户,提示“无法删除当前已连接的用户”
1. 首先查询出该用户的登录情况,注意用户名必须是大写 SQL> select username,sid,serial# from v$session where username = 'XST ...
- 使用富文本编辑器Kindeditor
今天在做需求的时候,遇到有一个字段,需要保存带有格式的内容,决定使用富文本框编辑器Kindeditor来实现,解决方法如下: 登录官网下载控件包: http://kindeditor.net/down ...
- 基于RBAC的权限框架
RBAC权限框架(Role-Based Access Control)基于角色的权限访问控制的框架,通过用户-角色-权限的关联,非常方便的进行权限管理,在这里不再说明什么是RBAC,请自行百度. 谢谢 ...
- .netcore持续集成测试篇之搭建内存服务器进行集成测试一
系列目录 在web项目里,我们把每一层的代码的单元测试都通过并不代表程序能正常运行,因为这个过程缺失了http管道,很多时候我们还还需要把项目布在iis环境中或者在vs里启动iis express服务 ...
- PCB学习总结
一.电子设计流程概述:项目立项------元件创库----------原理图设计-----------封装绘制-------pcb设计--------生产文件的输出--------pcb文件加工 二. ...
- 不得不会的10点Java基础知识
1.实例变量和类变量 实例变量:指每个对象独立的,修改其中一个对象的实例变量,不会影响其他实例变量的值,变量值无 static 关键字修饰: 类变量:是指所有对象共享的,其中一个对象把该变量的值修改了 ...
- windbg 使用与技巧
基本知识和常用命令 (1) Windbg下载地址http://msdn.microsoft.com/en-us/windows/hardware/gg463009.aspx 安装完后执行w ...
- net core Webapi基础工程搭建(四)——日志功能log4net
目录 前言 log4net 依然是,NuGet引用第三方类库 整合LogUtil 小结 前言 一个完整的项目工程离不开日志文件的记录,而记录文件的方法也有很多,可以自己通过Stream去实现文件的读写 ...