参考博文:《怎样深入理解堆和栈》
《关于寻址方式一篇就够了》
《堆栈、栈帧、函数调用过程》
《gdb 调试中-i frame命令之堆栈信息说明》
《【TARS】GDB 调试进阶「0x02」》
一个程序在运行过程中,操作系统会在内存中分配多个区域给这个程序。有以下几个区:
栈区(stack)
堆区(heap)
全局区(静态区)(static)
文字常量区
程序代码区
栈区是一段内存,主要作用是保存函数之间需要传递的参数并且保存被调用函数的返回地址。栈帧(stack )是一种逻辑划分,就是程序占用内存中的一个逻辑单元——特定一个函数所对应的栈区,它只保留一个函数的特定信息,一般会有局部变量、返回地址和旧的rbp值,甚至还有函数参数。
sudo lsb_release -a
看到操作系统是Ubuntu 22.04 LTS
。
sudo uname -r
看到内核版本是5.15.0-86-generic
。
sudo as --version
看到as
的版本是2.38
。
sudo ld --version
看到ld
的版本是2.38
。
sudo gcc --version
看到gcc
版本是11.2.0
。
sudo gdb --version
看到gdb
版本是12.1
。
runexponentInStack.s
(计算3的2次方)代码如下:
.global _start
.section .text
_start:
# 将rbp里边的值压入到栈中
pushq %rbp
movq %rsp,%rbp
# 请注意在C语言调用约定中,要是参数少于6个,不会使用栈来传递参数,这里只是为了说明栈的使用
# 将2压入到栈中,在下边exponent函数中可以使用24(%rbp)对值进行操作
pushq $2
# 将3压入到栈中,在下边exponent函数中可以使用16(%rbp)对值进行操作
pushq $3
call exponent
movq %rbp,%rsp
popq %rbp
# rdi里边会放入给操作系统的返回值,而此处是把rax里边的值放入到rdi
movq %rax,%rdi
# rax里边放入60,这是退出的系统调用号
movq $60,%rax
syscall
.global exponent
.type exponent,@function
.section .text
exponent:
# 将rbp里边的值压入到栈中
pushq %rbp
movq %rsp,%rbp
# rax = 1
movq $1,%rax
mainloop:
# 此处,mulq指令会计算rax乘以16(%rbp),最后乘积还会放到rax里边
# 16(%rbp) = 3
mulq 16(%rbp)
# 刚开始的时候,24(%rbp) = 2,这里表示将24(%rbp)-1,然后结果放到24(%rbp)里边
decq 24(%rbp)
# 如果24(%rbp)不等于0,那么就会跳转到mainloop
jnz mainloop
complete:
movq %rbp,%rsp
popq %rbp
ret
sudo vim runexponentInStack.s
把上边代码写入runexponentInStack.s
里边,sudo as -g runexponentInStack.s -o runexponentInStack.o
进行汇编,sudo ld -g runexponentInStack.o -o runexponentInStack
进行链接,sudo ./runexponentInStack
运行,sudo echo $?
查看运行结果。
这里使用栈在main
函数和exponent
函数之间传递两个参数,接下来一行代码一行代码分析堆栈图,使用sudo gdb -q ./runexponentInStack
开始进行分析对照。
而在gdb
中,bt
(backtrace
的缩写)命令可以查看当前函数向上的堆栈的函数的堆栈信息。i frame
(info frame
的缩写)可以查看当前调试函数的堆栈帧信息。
break 6
可以把断点打到第6行代码处,run
开始运行。
info frame
可以看到以下信息:
Stack level 0, frame at 0x7fffffffe668:
rip = 0x401000 in _start (runexponentInStack.s:5); saved rip = 0x1
source language asm.
Arglist at 0x7fffffffe658, args:
Locals at 0x7fffffffe658, Previous frame's sp is 0x7fffffffe668
Saved registers:
rip at 0x7fffffffe660
Stack level 0, frame at 0x7fffffffe668
表明当前_start
函数(也就是相当于C语言main
函数)的栈帧起始地址是0x7fffffffe668
。
rip = 0x401000 in _start (runexponentInStack.s:5); saved rip = 0x1
表明下一行执行的代码以内存0x401000
开始,调用_start
函数的内存地址以0x1
开始。
Arglist at 0x7fffffffe658, args:
表明参数变量的内存地址从0x7fffffffe658
开始。
Locals at 0x7fffffffe658, Previous frame's sp is 0x7fffffffe668
表明局部变量的内存地址从0x7fffffffe658
开始,而上一个函数的栈帧从0x7fffffffe668
开始。
Saved registers:
rip at 0x7fffffffe660
表示这个函数的调用位置是在内存0x7fffffffe660
处。
info register rsp
可以看到rsp
里边的值是0x7fffffffe660
,开始画堆栈图。
next
执行到下一步,这时是把旧的rbp
压到栈里边,info register rsp rbp
可以看到rsp
里边的值是0x7fffffffe658
,而rbp
的值是0x0
。
堆栈图如下:
这里可以知道16(%rbp)
等于rbp
+16
,24(%rbp)
等于rbp
+24
。
next
接着执行,发现rsp
和rbp
都没有改变,所以堆栈图也没有任何改变。
接下来连续按五下next
,都没涉及栈帧操作,堆栈图没有发生任何改变。
相当于全部把栈区还给操作系统了。
此文章为11月Day 12学习笔记,内容来源于极客时间《Rust 语言从入门到实战》。