给老外写邮件请教问题有关栈帧大小对齐的问题

说到x86-64,总不免要说说AMD的牛逼,x86-64是x86系列中集大成者,继承了向后兼容的优良传统,最早由AMD公司提出,代号AMD64;正是由于能向后兼容,AMD公司打了一场漂亮翻身战。导致Intel不得不转而生产兼容AMD64的CPU。这是IT行业以弱胜强的经典战役。不过,大家为了名称延续性,更习惯称这种系统结构为x86-64
X86-64在向后兼容的同时,更主要的是注入了全新的特性,特别的:x86-64有两种工作模式,32位OS既可以跑在传统模式中,把CPU当成i386来用;又可以跑在64位的兼容模式中,更加神奇的是,可以在32位的OS上跑64位的应用程序。有这种好事,用户肯定买账啦,
值得一提的是,X86-64开创了编译器的新纪元,在之前的时代里,Intel CPU的晶体管数量一直以摩尔定律在指数发展,各种新奇功能层出不穷,比如:条件数据传送指令cmovg,SSE指令等。但是GCC只能保守地假设目标机器的CPU是1985年的i386,额。。。这样编译出来的代码效率可想而知,虽然GCC额外提供了大量优化选项,但是这对应用程序开发者提出了很高的要求,会者寥寥。X86-64的出现,给GCC提供了一个绝好的机会,在新的x86-64机器上,放弃保守的假设,进而充分利用x86-64的各种特性,比如:在过程调用中,通过寄存器来传递参数,而不是传统的堆栈。又如:尽量使用条件传送指令,而不是控制跳转指令
寄存器简介
先明确一点,本文关注的是通用寄存器(后简称寄存器)。既然是通用的,使用并没有限制;后面介绍寄存器使用规则或者惯例,只是GCC(G++)遵守的规则。因为我们想对GCC编译的C(C++)程序进行分析,所以了解这些规则就很有帮助。
在体系结构教科书中,寄存器通常被说成寄存器文件,其实就是CPU上的一块存储区域,不过更喜欢使用标识符来表示,而不是地址而已。
X86-64中,所有寄存器都是64位,相对32位的x86来说,标识符发生了变化,比如:从原来的%ebp变成了%rbp。为了向后兼容性,%ebp依然可以使用,不过指向了%rbp的低32位。
X86-64寄存器的变化,不仅体现在位数上,更加体现在寄存器数量上。新增加寄存器%r8到%r15。加上x86的原有8个,一共16个寄存器。
刚刚说到,寄存器集成在CPU上,存取速度比存储器快好几个数量级,寄存器多了,GCC就可以更多的使用寄存器,替换之前的存储器堆栈使用,从而大大提升性能。
让寄存器为己所用,就得了解它们的用途,这些用途都涉及函数调用,X86-64有16个64位寄存器,分别是:%rax,%rbx,%rcx,%rdx,%esi,%edi,%rbp,%rsp,%r8,%r9,%r10,%r11,%r12,%r13,%r14,%r15。其中:
%rax 作为函数返回值使用。
%rsp 栈指针寄存器,指向栈顶
%rdi,%rsi,%rdx,%rcx,%r8,%r9 用作函数参数,依次对应第1参数,第2参数。。。
%rbx,%rbp,%r12,%r13,%14,%15 用作数据存储,遵循被调用者使用规则,简单说就是随便用,调用子函数之前要备份它,以防他被修改
%r10,%r11 用作数据存储,遵循调用者使用规则,简单说就是使用之前要先保存原值
C语言属于面向过程语言,他最大特点就是把一个程序分解成若干过程(函数),比如:入口函数是main,然后调用各个子函数。在对应机器语言中,GCC把过程转化成栈帧(frame),简单的说,每个栈帧对应一个过程。X86-32典型栈帧结构中,由%ebp指向栈帧开始,%esp指向栈顶。
函数进入和返回
函数的进入和退出,通过指令call和ret来完成,给一个例子
#include &/code&
int foo ( int x )
int array[] = {1,3,5};
return array[x];
end of function foo
int main ( int argc, char *argv[] )
int i = 1;
int j = foo(i);
fprintf(stdout, &i=%d,j=%d\n&, i, j);
return EXIT_SUCCESS;
/* ----------
end of function main
---------- */
命令行中调用gcc,生成汇编语言:
Shell & gcc –S –o test.s test.c
Main函数第40行的指令Call foo其实干了两件事情:
Pushl %rip
//保存下一条指令(第41行的代码地址)的地址,用于函数返回继续执行
//跳转到函数foo
Foo函数第19行的指令ret 相当于:
//恢复指令指针寄存器
栈帧的建立和撤销
还是上一个例子,看看栈帧如何建立和撤销
说题外话,以”点”做为前缀的指令都是用来指导汇编器的命令。无意于程序理解,统统忽视之,比如第31行。
栈帧中,最重要的是帧指针%ebp和栈指针%esp,有了这两个指针,我们就可以刻画一个完整的栈帧
函数main的第30~32行,描述了如何保存上一个栈帧的帧指针,并设置当前的指针。
第49行的leave指令相当于:
Movq %rbp %rsp
//撤销栈空间,回滚%rsp
//恢复上一个栈帧的%rbp
同一件事情会有很多的做法,GCC会综合考虑,并作出选择。选择leave指令,极有可能因为该指令需要存储空间少,需要时钟周期也少。
你会发现,在所有的函数中,几乎都是同样的套路,
我们通过gdb观察一下进入foo函数之前main的栈帧,进入foo函数的栈帧,退出foo的栈帧情况
Shell& gcc -g -o test test.c
Shell& gdb --args test
Gdb & break main
进入foo函数之前:
你会发现rbp-rsp=0×20,这个是由代码第11行造成的。
进入foo函数的栈帧:
回到main函数的栈帧,rbp和rsp恢复成进入foo之前的状态,就好像什么都没发生一样。
可有可无的帧指针
你刚刚搞清楚帧指针,是不是很期待要马上派上用场,这样你可能要大失所望,因为大部分的程序,都加了优化编译选项:-O2,这几乎是普遍的选择。在这种优化级别,甚至更低的优化级别-O1,都已经去除了帧指针,也就是%ebp中再也不是保存帧指针,而且另作他途。
在x86-32时代,当前栈帧总是从保存%ebp开始,空间由运行时决定,通过不断push和pop改变当前栈帧空间;x86-64开始,GCC有了新的选择,优化编译选项-O1,可以让GCC不再使用栈帧指针,下面引用 gcc manual 一段话 :
-O also turns on -fomit-frame-pointer on machines where doing so does not interfere with debugging.
这样一来,所有空间在函数开始处就预分配好,不需要栈帧指针;通过%rsp的偏移就可以访问所有的局部变量。
说了这么多,还是看看例子吧。同一个例子, 加上-O1选项:
Shell&: gcc –O1 –S –o test.s test.c
分析main函数,GCC分析发现栈帧只需要8个字节,于是进入main之后第一条指令就分配了空间(第23行):
Subq $8, %rsp
然后在返回上一栈帧之前,回收了空间(第34行):
Addq $8, %rsp
等等,为啥main函数中并没有对分配空间的引用呢?这是因为GCC考虑到栈帧对齐需求,故意做出的安排。
再来看foo函数,这里你可以看到%rsp是如何引用栈空间的。
等等,不是需要先预分配空间吗?这里为啥没有预分配,直接引用栈顶之外的地址?
这就要涉及x86-64引入的牛逼特性了。
访问栈顶之外
通过readelf查看可执行程序的header信息:
红色区域部分指出了x86-64遵循ABI规则的版本,它定义了一些规范,遵循ABI的具体实现应该满足这些规范,其中,他就规定了程序可以使用栈顶之外128字节的地址。
这说起来很简单,具体实现可有大学问,这超出了本文的范围,具体大家参考虚拟存储器。别的不提,接着上例,我们发现GCC利用了这个特性,干脆就不给foo函数分配栈帧空间了,而是直接使用栈帧之外的空间。@恨少说这就相当于内联函数呗,我要说:这就是编译优化的力量。
寄存器保存惯例
过程调用中,调用者栈帧需要寄存器暂存数据,被调用者栈帧也需要寄存器暂存数据。如果调用者使用了%rbx,那被调用者就需要在使用之前把%rbx保存起来,然后在返回调用者栈帧之前,恢复%rbx。遵循该使用规则的寄存器就是被调用者保存寄存器,对于调用者来说,%rbx就是非易失的。
反过来,调用者使用%r10存储局部变量,为了能在子函数调用后还能使用%r10,调用者把%r10先保存起来,然后在子函数返回之后,再恢复%r10。遵循该使用规则的寄存器就是调用者保存寄存器,对于调用者来说,%r10就是易失的,
举个例子:
#include &stdio.h&
#include &stdlib.h&
void sfact_helper ( long int x, long int * resultp)
*resultp = 1;
sfact_helper(x-1,&nresult);
*resultp = x *
end of function foo
sfact ( long int x )
sfact_helper(x, &result);
end of function sfact
main ( int argc, char *argv[] )
int sum = sfact(10);
fprintf(stdout, &sum=%d\n&, sum);
return EXIT_SUCCESS;
/* ----------
end of function main
---------- */
命令行中调用gcc,生成汇编语言:
Shell&: gcc –O1 –S –o test2.s test2.c
在函数sfact_helper中,用到了寄存器%rbx和%rbp,在覆盖之前,GCC选择了先保存他们的值,代码6~9说明该行为。在函数返回之前,GCC依次恢复了他们,就如代码27-28展示的那样。
看这段代码你可能会困惑?为什么%rbx在函数进入的时候,指向的是-16(%rsp),而在退出的时候,变成了32(%rsp) 。上文不是介绍过一个重要的特性吗?访问栈帧之外的空间,这是GCC不用先分配空间再使用;而是先使用栈空间,然后在适当的时机分配。第11行代码展示了空间分配,之后栈指针发生变化,所以同一个地址的引用偏移也相应做出调整。
X86时代,参数传递是通过入栈实现的,相对CPU来说,存储器访问太慢;这样函数调用的效率就不高,在x86-64时代,寄存器数量多了,GCC就可以利用多达6个寄存器来存储参数,多于6个的参数,依然还是通过入栈实现。了解这些对我们写代码很有帮助,起码有两点启示:
尽量使用6个以下的参数列表,不要让GCC为难啊。
传递大对象,尽量使用指针或者引用,鉴于寄存器只有64位,而且只能存储整形数值,寄存器存不下大对象
让我们具体看看参数是如何传递的:
#include &stdio.h&
#include &stdlib.h&
int foo ( int arg1, int arg2, int arg3, int arg4, int arg5, int arg6, int arg7 )
int array[] = {100,200,300,400,500,600,700};
int sum = array[arg1] + array[arg7];
end of function foo
main ( int argc, char *argv[] )
int i = 1;
int j = foo(0, 1, 2, 3, 4, 5, 6);
fprintf(stdout, &i=%d,j=%d\n&, i, j);
return EXIT_SUCCESS;
/* ----------
end of function main
---------- */
命令行中调用gcc,生成汇编语言:
Shell&: gcc –O1 –S –o test1.s test1.c
Main函数中,代码31~37准备函数foo的参数,从参数7开始,存储在栈上,%rsp指向的位置;参数6存储在寄存器%r9d;参数5存储在寄存器%r8d;参数4对应于%ecx;参数3对应于%edx;参数2对应于%esi;参数1对应于%edi。
Foo函数中,代码14-15,分别取出参数7和参数1,参与运算。这里数组引用,用到了最经典的寻址方式,-40(%rsp,%rdi,4)=%rsp + %rdi *4 + (-40);其中%rsp用作数组基地址;%rdi用作了数组的下标;数字4表示sizeof(int)=4。
结构体传参
应@桂南要求,再加一节,相信大家也很想知道结构体是如何存储,如何引用的,如果作为参数,会如何传递,如果作为返回值,又会如何返回。
看下面的例子:
#include &stdio.h&
#include &stdlib.h&
struct demo_s {
char var8;
long var64;
struct demo_s foo (struct demo_s d)
d.var32=32;
d.var64=64;
end of function foo
main ( int argc, char *argv[] )
struct demo_s d,
result = foo (d);
fprintf(stdout, &demo: %d, %d, %ld\n&, result.var8, result.var32, result.var64);
return EXIT_SUCCESS;
/* ----------
end of function main
---------- */
我们缺省编译选项,加了优化编译的选项可以留给大家思考。
-S -o test.s test.c
上面的代码加了一些注释,方便大家理解,
问题1:结构体如何传递?它被分成了两个部分,var8和var32合并成8个字节的大小,放在寄存器%rdi中,var64放在寄存器的%rsi中。也就是结构体分解了。
问题2:结构体如何存储? 注意看foo函数的第15~17行注意到,结构体的引用变成了一个偏移量访问。这和数组很像,只不过他的元素大小可变。
问题3:结构体如何返回,原本%rax充当了返回值的角色,现在添加了返回值2:%rdx。同样,GCC用两个寄存器来表示结构体。
恩, 即使在缺省情况下,GCC依然是想尽办法使用寄存器。随着结构变的越来越大,寄存器不够用了,那就只能使用栈了。
了解寄存器和栈帧的关系,对于gdb调试很有帮助;过些日子,一定找个合适的例子和大家分享一下。
1. 深入理解计算机体系结构
2. x86系列汇编语言程序设计
A quality product by新手园地& & & 硬件问题Linux系统管理Linux网络问题Linux环境编程Linux桌面系统国产LinuxBSD& & & BSD文档中心AIX& & & 新手入门& & & AIX文档中心& & & 资源下载& & & Power高级应用& & & IBM存储AS400Solaris& & & Solaris文档中心HP-UX& & & HP文档中心SCO UNIX& & & SCO文档中心互操作专区IRIXTru64 UNIXMac OS X门户网站运维集群和高可用服务器应用监控和防护虚拟化技术架构设计行业应用和管理服务器及硬件技术& & & 服务器资源下载云计算& & & 云计算文档中心& & & 云计算业界& & & 云计算资源下载存储备份& & & 存储文档中心& & & 存储业界& & & 存储资源下载& & & Symantec技术交流区安全技术网络技术& & & 网络技术文档中心C/C++& & & GUI编程& & & Functional编程内核源码& & & 内核问题移动开发& & & 移动开发技术资料ShellPerlJava& & & Java文档中心PHP& & & php文档中心Python& & & Python文档中心RubyCPU与编译器嵌入式开发驱动开发Web开发VoIP开发技术MySQL& & & MySQL文档中心SybaseOraclePostgreSQLDB2Informix数据仓库与数据挖掘NoSQL技术IT业界新闻与评论IT职业生涯& & & 猎头招聘IT图书与评论& & & CU技术图书大系& & & Linux书友会二手交易下载共享Linux文档专区IT培训与认证& & & 培训交流& & & 认证培训清茶斋投资理财运动地带快乐数码摄影& & & 摄影器材& & & 摄影比赛专区IT爱车族旅游天下站务交流版主会议室博客SNS站务交流区CU活动专区& & & Power活动专区& & & 拍卖交流区频道交流区
UID空间积分0 积分940阅读权限20帖子精华可用积分940 信誉积分1567 专家积分0 在线时间1601 小时注册时间最后登录
丰衣足食, 积分 940, 距离下一级还需 60 积分
帖子主题精华可用积分940 信誉积分1567 专家积分0 在线时间1601 小时注册时间最后登录
论坛徽章:10
一个例子:vi stack.c复制代码int sum (int x, int y)
{
& & int a = 0;
& & a =
& & a +=
& &
}
int main (int argc, char *argv[])
{
& & int x, y,
& & x = 0x12;
& & y = 0x34;
& & result = sum (x, y);
& & return 0;
}复制代码gcc -m32 stack.c -o stack复制代码objdump -d stack & stack.dump复制代码 &main&:
80483b2:& &55& && && && && && && & push& &%ebp
80483b3:& &89 e5& && && && && && & mov& & %esp,%ebp
80483b5:& &83 ec 18& && && && && & sub& & $0x18,%esp
80483b8:& &c7 45 f4 12 00 00 00& & movl& &$0x12,-0xc(%ebp)
80483bf:& &c7 45 f8 34 00 00 00& & movl& &$0x34,-0x8(%ebp)
80483c6:& &8b 45 f8& && && && && & mov& & -0x8(%ebp),%eax
80483c9:& &89 44 24 04& && && && & mov& & %eax,0x4(%esp)
80483cd:& &8b 45 f4& && && && && & mov& & -0xc(%ebp),%eax
80483d0:& &89 04 24& && && && && & mov& & %eax,(%esp)
80483d3:& &e8 bc ff ff ff& && && & call& &8048394 &sum&
80483d8:& &89 45 fc& && && && && & mov& & %eax,-0x4(%ebp)
80483db:& &b8 00 00 00 00& && && & mov& & $0x0,%eax
80483e0:& &c9& && && && && && && & leave&&
80483e1:& &c3& && && && && && && & ret复制代码汇编指令中的sub& & $0x18,%esp为main()的栈帧预留了0x18字节的空间,这里一直没想明白,网上有些资料说ABI约定栈帧按照16字节对齐,哪位大神帮忙解释一下这段代码中的0x18字节是怎样得出的,16字节边界对齐的原理是怎样的?谢谢。
&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp&&nbsp|&&nbsp
UID空间积分0 积分3473阅读权限50帖子精华可用积分3473 信誉积分516 专家积分0 在线时间233 小时注册时间最后登录
小富即安, 积分 3473, 距离下一级还需 1527 积分
帖子主题精华可用积分3473 信誉积分516 专家积分0 在线时间233 小时注册时间最后登录
论坛徽章:4
为什么对齐到16字节:
为什么是预留0x18(24)字节的空间?
x,y,result各4字节,sum的两个参数各4字节,总共为20字节。
一般为了对齐到16字节是这样的:and& & $0xfffffff0,%esp,然后减去一个16倍数的字节,这里减去的不是16的倍数,它怎样保证对齐到16字节?
我猜测它是这样计算的:假设调用它的上层的函数是对齐到16字节的,
这样加上函数的返回地址和开始的push& &%ebp,就余下了8个字节,再减上24字节就对齐到了16字节.
UID711642空间积分0 积分9461阅读权限100帖子精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
帖子主题精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
认证徽章论坛徽章:17
本帖最后由 myworkstation 于
10:48 编辑
井蛙夏虫 发表于
回复 1# superwujc
为什么对齐到16字节:
补充一下对齐到16字节是某些CPU指令集要求的。
Explicitly-aligned SIMD load and store instructions accessing 16 bytes of memory (e.g. MOVAPD, MOVAPS,
MOVDQA, etc.). These instructions always require memory address to be aligned on 16-byte boundary.
The vast majority of arithmetic and data processing instructions in legacy SSE instructions (non-VEX-encoded
SIMD instructions) support memory access semantics. When these instructions access 16 bytes of data from
memory, the memory address must be aligned on 16-byte boundary.
amd64-ABI中的描述:
In addition to registers, each function has a frame on the run-time stack. This stack
grows downwards from high addresses. Figure 3.3 shows the stack organization.
The end of the input argument area shall be aligned on a 16 byte boundary.
In other words, the value (%rsp −
is always a multiple of 16 when control is
transferred to the function entry point. The stack pointer, %rsp, always points to
the end of the latest allocated stack frame
UID9730150空间积分0 积分1877阅读权限30帖子精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
家境小康, 积分 1877, 距离下一级还需 123 积分
帖子主题精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
论坛徽章:1
myworkstation 发表于
补充一下对齐到16字节是某些CPU指令集要求的。
Explicitly-aligned SIMD load and store instructio ...
void func1()
{& && && && &
& &char x1;&&
}& && && && &
& && && && &
void func2()
{& && && && &
& &func1();&&
}& && && && &
& && && && &
int main()& &
{& && && && &
& &func2();&&
Dump of assembler code for function main():
0x &main()+0&:&&lea& & 0x4(%esp),%ecx
0x0804840a &main()+4&:&&and& & $0xfffffff0,%esp
0x0804840d &main()+7&:&&pushl&&-0x4(%ecx)
0x &main()+10&: push& &%ebp
0x &main()+11&: mov& & %esp,%ebp
0x &main()+13&: push& &%ecx
0x &main()+14&: call& &0x80483fc &func2()&
0x &main()+19&: mov& & $0x0,%eax
0x0804841e &main()+24&: pop& & %ecx
0x0804841f &main()+25&: pop& & %ebp
0x &main()+26&: lea& & -0x4(%ecx),%esp
0x &main()+29&: ret
Dump of assembler code for function func2():
0x080483fc &func2()+0&: push& &%ebp
0x080483fd &func2()+1&: mov& & %esp,%ebp
0x080483ff &func2()+3&: call& &0x80483f4 &func1()&
0x &func2()+8&: pop& & %ebp
0x &func2()+9&: ret
Dump of assembler code for function func1():
0x &func1()+0&: push& &%ebp
0x &func1()+1&: mov& & %esp,%ebp
0x &func1()+3&: sub& & $0x10,%esp
0x080483fa &func1()+6&: leave&&
0x080483fb &func1()+7&: ret&&
你会发现,根本没有对齐的。
gcc 版本 4.1.2
(Red Hat 4.1.2-4
Linux 2.6.18-194.el5xen #1 SMP Tue Mar 16 22:08:06 EDT
i686 i386 GNU/Linux
我一直没搞懂这个对齐是怎么回事儿。
gcc的手册讲得很含糊
UID空间积分0 积分38168阅读权限90帖子精华可用积分38168 信誉积分1794 专家积分0 在线时间2055 小时注册时间最后登录
巨富豪门, 积分 38168, 距离下一级还需 1832 积分
帖子主题精华可用积分38168 信誉积分1794 专家积分0 在线时间2055 小时注册时间最后登录
论坛徽章:4
你丫研究编译器的。
专注linux服务端性能与架构设计...
UID711642空间积分0 积分9461阅读权限100帖子精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
帖子主题精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
认证徽章论坛徽章:17
chenzhanyiczy // 简单分析几条指令
lea& & 0x4(%esp),%ecx
// 0x4(%esp)指向是函数返回地址,由于是在main函数中所以实际上这个地址指向了stack顶端。这句话把函数返回地址保存到ecx寄存器,
and& & $0xfffffff0,%esp 
//0xfffffff是二进制的-16,这句话是让stack在16字节边界对齐
sub& & $0x10,%esp
// 0x10实际上是16,虽然只有一个char局部变量,但依旧对齐到16 bytes的边界上
请参考:
/Bufferoverflowc/bufferoverflowvulexploitdemo31.html
http://unixresources.net/linux/clf/linuxK/archive/00/00/39/26/392651.html复制代码
UID9730150空间积分0 积分1877阅读权限30帖子精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
家境小康, 积分 1877, 距离下一级还需 123 积分
帖子主题精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
论坛徽章:1
本帖最后由 chenzhanyiczy 于
21:40 编辑
myworkstation 发表于
回复 4# chenzhanyiczy
Dump of assembler code for function main():
0x &main()+0&:&&lea& & 0x4(%esp),%ecx
0x0804840a &main()+4&:&&and& & $0xfffffff0,%esp& &&&--& 对齐16字节,也esp是16字节对齐
0x0804840d &main()+7&:&&pushl&&-0x4(%ecx)& && && & --& 压栈4字节
0x &main()+10&: push& &%ebp& && && && && && &--& 压栈4字节
0x &main()+11&: mov& & %esp,%ebp
0x &main()+13&: push& &%ecx& && && && && && &--& 压栈4字节
0x &main()+14&: call& &0x80483fc &func2()&& &--& 压栈4字节
0x &main()+19&: mov& & $0x0,%eax
0x0804841e &main()+24&: pop& & %ecx
0x0804841f &main()+25&: pop& & %ebp
0x &main()+26&: lea& & -0x4(%ecx),%esp
0x &main()+29&: ret
Dump of assembler code for function func2():
0x080483fc &func2()+0&: push& &%ebp& && && && && && &--& 压栈4字节
0x080483fd &func2()+1&: mov& & %esp,%ebp
0x080483ff &func2()+3&: call& &0x80483f4 &func1()&
0x &func2()+8&: pop& & %ebp
0x &func2()+9&: ret
当eip在0x080483ff的时候,esp的地址已经不是16字节对齐了,也即进入func1的时候,esp也不是16字节对齐了,也即func1的栈不是16字节对齐了。
这就跟下面粗体描述的相冲突了:
amd64-ABI中的描述:
In addition to registers, each function has a frame on the run-time stack. This stack
grows downwards from high addresses. Figure 3.3 shows the stack organization.
The end of the input argument area shall be aligned on a 16 byte boundary.
In other words, the value (%rsp −&&is always a multiple of 16 when control is
transferred to the function entry point. The stack pointer, %rsp, always points to
the end of the latest allocated stack frame
To ensure proper alignment of this values on the stack, the stack boundary must
be as aligned as that required by any value stored on the stack. Further, every
function must be generated such that it keeps the stack aligned.
UID711642空间积分0 积分9461阅读权限100帖子精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
帖子主题精华可用积分9461 信誉积分1634 专家积分0 在线时间1022 小时注册时间最后登录
认证徽章论坛徽章:17
chenzhanyiczy
& & 你的理解有问题,这个对齐是从特定地址开始算的。就通常的函数调用代码stack frame来讲是在push& &%ebp之后的地址要保证16节字对齐,而fun2函数内之所以没有对齐的代码是因为没有局部变量(具体的说是就没有使用变量所以不直接使用stack,所以这个过程省略了,在其下一级调用fun1中进行对齐)。对每个函数调用stack frame来讲如果有stack变量那么stack变量所使用的地址需要对齐,不包括隐式的函数返回地址以及常见的push ebp。
UID9730150空间积分0 积分1877阅读权限30帖子精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
家境小康, 积分 1877, 距离下一级还需 123 积分
帖子主题精华可用积分1877 信誉积分266 专家积分35 在线时间678 小时注册时间最后登录
论坛徽章:1
本帖最后由 chenzhanyiczy 于
01:14 编辑
myworkstation 发表于
回复 7# chenzhanyiczy
1、“fun2函数内之所以没有对齐的代码是因为没有局部变量(具体的说是就没有使用变量所以不直接使用stack,所以这个过程省略了,在其下一级调用fun1中进行对齐)”
-& 这结论根据是什么? 哪个手册说了,没有局部变量就是不会使用stack,就不会栈对齐。有些函数即使没有局部变量,也会有类似sub 0x30 %esp的指令开辟栈空间,姑且不论它的作用。
2、Dump of assembler code for function func1():
0x &func1()+0&: push& &%ebp
0x &func1()+1&: mov& & %esp,%ebp
0x &func1()+3&: sub& & $0x10,%esp
0x080483fa &func1()+6&: leave&&
0x080483fb &func1()+7&: ret&&
ok,即使1说的正确,那这里func1的局部变量地址16字节对齐了吗? 也没有对齐。
你仔细看看汇编语句。
UID空间积分0 积分3473阅读权限50帖子精华可用积分3473 信誉积分516 专家积分0 在线时间233 小时注册时间最后登录
小富即安, 积分 3473, 距离下一级还需 1527 积分
帖子主题精华可用积分3473 信誉积分516 专家积分0 在线时间233 小时注册时间最后登录
论坛徽章:4
chenzhanyiczy
默认情况下,对齐是建议,并非强制
我没有你用的版本的编译器,你编译一下下面这个程序,把汇编代码发上来看看void func1();
void func2();
& &
int main()& &
{& && && && &
& &func2();&&
}
void func2()
{& && && && &
& &func1();&&
}
void func1()
{& && && && &
& &char x1;&&
& &&&
}复制代码

我要回帖

更多关于 请教问题 英文 的文章

 

随机推荐