- 第一章 CPU 简介
- 第二章 Hello,world!
- 第三章 函数开始和结束
- 第四章 栈
- Chapter 5 printf() 与参数处理
- Chapter 6 scanf()
- CHAPER7 访问传递参数
- Chapter 8 一个或者多个字的返回值
- Chapter 9 指针
- Chapter 10 条件跳转
- 第 11 章 选择结构 switch()/case/default
- 第 12 章 循环结构
- 第 13 章 strlen()
- Chapter 14 Division by 9
- chapter 15 用 FPU 工作
- Chapter 16 数组
- Chapter 17 位域
- 第 18 章 结构体
- 19 章 联合体
- 第二十章 函数指针
- 第 21 章 在 32 位环境中的 64 位值
- 第二十二章 SIMD
- 23 章 64 位化
- 24 章 使用 x64 下的 SIMD 来处理浮点数
- 25 章 温度转换
- 26 章 C99 的限制
- 27 章 内联函数
- 第 28 章 得到不正确反汇编结果
- 第 29 章 花指令
- 第 30 章 16 位 Windows
- 第 31 章 类
- 三十二 ostream
- 34.2.2 MSVC
- 34.2.3 C++ 11 std::forward_list
- 34.3 std::vector
- 34.4 std::map and std::set
4.2 栈可以用来做什么?
4.2.1 保存函数返回地址以便在函数执行完成时返回控制权
x86
当使用 CALL 指令去调用一个函数时,CALL 后面一条指令的地址会被保存到栈中,使用无条件跳转指令跳转到 CALL 中执行。 ? ?CALL 指令等价于 PUSH 函数返回地址和 JMP 跳转。
#!cpp
void f()
{
f();
};
?MSVC 2008 显示的问题:
#!bash
c:\tmp6>cl ss.cpp /Fass.asm
Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86
Copyright (C) Microsoft Corporation. All rights reserved.
ss.cpp
c:\tmp6\ss.cpp(4) : warning C4717: ’f’ : recursive on all control paths, function will cause
runtime stack overflow
但无论如何还是生成了正确的代码:
#!bash
?f@@YAXXZ PROC ; f
; File c:\tmp6\ss.cpp
; Line 2
push ebp
mov ebp, esp
; Line 3
call ?f@@YAXXZ ; f
; Line 4
pop ebp
ret 0
?f@@YAXXZ ENDP ; f
??如果我们设置优化(/0x) 标识,生成的代码将不会出现栈溢出,并且会运行的很好。
#!bash
?f@@YAXXZ PROC ; f
; File c:\tmp6\ss.cpp
; Line 2
$LL3@f:
; Line 3
jmp SHORT $LL3@f
?f@@YAXXZ ENDP ; f
GCC 4.4.1 在这两种条件下,会生成同样的代码,而且不会有任何警告。
ARM
ARM 程序员经常使用栈来保存返回地址,但有些不同。像是提到的“Hello,World!(2.3), RA 保存在 LR(链接寄存器)。然而,如果需要调用另外一个函数,需要多次使用 LR 寄存器,它的值会被保存起来。通常会在函数开始的时候保存。像我们经常看到的指令“ PUSH R4-R7, LR
”,在函数结尾处的指令“ POP R4-R7, PC
”,在函数中使?用到的寄存器会被保存到栈中,包括 LR。
尽管如此,如果一个函数从未调用其它函数,在 ARM 专用术语中被叫称作叶子函数。因此,叶?函数不需要 LR 寄存器。如果一个函数很小并使用了少量的寄存器,可能不会?到栈。因此,是可以不使用栈而实现调用叶子函数的。在扩展 ARM 上不使用栈,这样就会比在 x86 上运行要快。在未分配栈内存或栈内存不可用的情况下,这种方式是非常有用的。
4.2.2 传递函数参数
在 x86 中,最常见的传参方式是“cdecl”:
#!bash
push arg3
push arg2
push arg1
call f
add esp, 4*3
被调用函数通过栈指针访问函数参数。因此,这就是为什么要在函数 f() 执行之前将数据放入栈中的原因。
来看一下其它调用约定。没有意义也没有必要强迫程序员一定要使用栈来传递参数。
这不是必需的,可以不使用栈,通过其它方式来实现。
例如,可以为参数分配一部分堆空间,存入参数,将指向这部分内存的指针存入 EAX,这样就可以了。然而,在 x86 和 ARM 中,使用栈传递参数还是更加方便的。
另外一个问题,被调用的函数并不知道有多少参数被传递进来。有些函数可以传递不同个数的参数(如:printf()),通过一些说明性的字符(以%开始) 才可以判断。如果我们这样调用函数
#!cpp
printf("%d %d %d", 1234);
printf()
会传?入 1234,然后另外传入栈中的两个随机数字。这就让我们使用哪种方式调用 main() 函数变得不重要,像 main(),main(int argc, char *argv[])
或 main(int argc, char *argv[], char *envp[])
。
事实上,CRT 函数在调?main() 函数时,使用了下面的指令: ???? #!bash push envp push argv push argc call main ...
?如果你使用了没有参数的 main() 函数,尽管如此,但它们仍然在栈中,只是无法使用。如果你使用了 main(int argc, char *argv[])
,你可以使用两个参数,第三个参数对你的函数是“不可见的”。如果你使用 main(int argc)
这种方式,同样是可以正常运?的。
4.2.3 局部变量存放
局部变量存放到任何你想存放的地方,但传统上来说,大家更喜欢通过将栈指针移动到栈底,来存放局部变量,当然,这不是必需的。
4.2.4 x86: alloca() 函数
对 alloca() 函数并没有值得学习的。
该函数的作用像 malloc() 一样,但只会在栈上分配内存。
它分配的内存并不需要在函数结束时,调用像 free() 这样的函数来释放,当函数运行结束,ESP 的值还原时,这部分内存会自动释放。对 alloca() 函数的实现也没有什么值得介绍的。
这个函数,如果精简一下,就是将 ESP 指针指向栈底,根据你所需要的内存大小将 ESP 指向所分配的内存块。让我们试一下:
#!cpp
#include <malloc.h>
#include <stdio.h>
void f() {
char *buf=(char*)alloca (600);
_snprintf (buf, 600, "hi! %d, %d, %d\n", 1, 2, 3);
puts (buf);
};
( _snprintf()
函数作用与 printf() 函数基本相同,不同的地方是 printf() 会将结果输出到的标准输出 stdout,? _snprintf()
会将结果保存到内存中,后面两?代码可以使用 printf() 替换,但我想说明小内存的使用习惯。)
MSVC
让我们来编译 (MSVC 2010):
#!bash
...
??????? mov eax, 600 ; 00000258H
call __alloca_probe_16
mov esi, esp
??????? push 3
push 2
push 1
push OFFSET $SG2672
push 600 ; 00000258H
push esi
call __snprintf
??????? push esi
call _puts
add esp, 28 ; 0000001cH
...
? ??这唯一的函数参数是通过 EAX(未使用栈) 传递。在函数调用结束时,ESP 会指向 600 字节的内存,我们可以像使用一般内存一样来使用它做为缓冲区。
GCC + Intel 格式
GCC 4.4.1 不需要调用函数就可以实现相同的功能:
#!bash
.LC0:
.string "hi! %d, %d, %d\n"
f:
push ebp
mov ebp, esp
push ebx
sub esp, 660
lea ebx, [esp+39]
and ebx, -16 ; align pointer by 16-bit border
mov DWORD PTR [esp], ebx ; s
mov DWORD PTR [esp+20], 3
mov DWORD PTR [esp+16], 2
mov DWORD PTR [esp+12], 1
mov DWORD PTR [esp+8], OFFSET FLAT:.LC0 ; "hi! %d, %d, %d\n"
mov DWORD PTR [esp+4], 600 ; maxlen
call _snprintf
mov DWORD PTR [esp], ebx
call puts
mov ebx, DWORD PTR [ebp-4]
leave
ret
?####GCC + AT&T 格式
我们来看相同的代码,但使用了 AT&T 格式:
#!bash
.LC0:
.string "hi! %d, %d, %d\n"
f:
pushl %ebp
movl %esp, %ebp
pushl %ebx
subl $660, %esp
leal 39(%esp), %ebx
andl $-16, %ebx
movl %ebx, (%esp)
movl $3, 20(%esp)
movl $2, 16(%esp)
movl $1, 12(%esp)
movl $.LC0, 8(%esp)
movl $600, 4(%esp)
call _snprintf
movl %ebx, (%esp)
call puts
movl -4(%ebp), %ebx
leave
ret
?代码与上面的那个图是相同的。
例如: movl $3, 20(%esp)
与 mov DWORD PTR [esp + 20],3
是等价的,Intel 的内存地址增加是使用 register+offset,而 AT&T 使用的是 offset(%register)。
4.2.5 (Windows) 结构化异常处理
SEH 也是存放在栈中的(如果存在的话)。 想了解更多,请等待后续翻译在(51.3)。
4.2.6 缓冲区溢出保护
想了解更多,请等待后续翻译,在(16.2)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论