编译原理之美
宫文学
北京原点代码 CEO
46197 人已学习
新⼈⾸单¥59
登录后,你可以任选4讲全文学习
课程目录
已完结/共 45 讲
开篇词 (1讲)
编译原理 · 期中考试周 (1讲)
编译原理之美
15
15
1.0x
00:00/00:00
登录|注册

加餐 | 汇编代码编程与栈帧管理

实现高级语言难以实现的结果
使用汇编语言编程的场景
浮点数计算
循环和if语句
栈的管理
函数调用
返回值处理
浮点数运算指令
浮点数传参
条件跳转指令
if语句转换成汇编
条件跳转指令
EFLAGS寄存器
循环语句转换成汇编
超过6个参数的处理
寄存器传参
参数传递机制
栈的管理
函数调用尾声
参数传递
设置栈指针
函数调用序曲
常用寄存器
汇编指令
构成元素
一课一思
课程小结
示例5:浮点数的使用
示例4:if语句的汇编码解析
示例3:循环语句的汇编码解析
示例2:同时使用寄存器和栈来传参
示例1:过程调用和栈帧
汇编语言基础知识
汇编代码编程与栈帧管理

该思维导图由 AI 生成,仅供参考

22 讲中,我们侧重讲解了汇编语言的基础知识,包括构成元素、汇编指令和汇编语言中常用的寄存器。学习完基础知识之后,你要做的就是多加练习,和汇编语言“混熟”。小窍门是查看编译器所生成的汇编代码,跟着学习体会。
不过,可能你是初次使用汇编语言,对很多知识点还会存在疑问,比如:
在汇编语言里调用函数(过程)时,传参和返回值是怎么实现的呢?
21 讲中运行期机制所讲的栈帧,如何通过汇编语言实现?
条件语句和循环语句如何实现?
……
为此,我策划了一期加餐,针对性地讲解这样几个实际场景,希望帮你加深对汇编语言的理解。

示例 1:过程调用和栈帧

这个例子涉及了一个过程调用(相当于 C 语言的函数调用)。过程调用是汇编程序中的基础结构,它涉及到栈帧的管理、参数的传递这两个很重要的知识点。
假设我们要写一个汇编程序,实现下面 C 语言的功能:
/*function-call1.c */
#include <stdio.h>
int fun1(int a, int b){
int c = 10;
return a+b+c;
}
int main(int argc, char *argv[]){
printf("fun1: %d\n", fun1(1,2));
return 0;
}
fun1 函数接受两个整型的参数:a 和 b,来看看这两个参数是怎样被传递过去的,手写的汇编代码如下:
# function-call1-craft.s 函数调用和参数传递
# 文本段,纯代码
.section __TEXT,__text,regular,pure_instructions
_fun1:
# 函数调用的序曲,设置栈指针
pushq %rbp # 把调用者的栈帧底部地址保存起来
movq %rsp, %rbp # 把调用者的栈帧顶部地址,设置为本栈帧的底部
subq $4, %rsp # 扩展栈
movl $10, -4(%rbp) # 变量c赋值为10,也可以写成 movl $10, (%rsp)
# 做加法
movl %edi, %eax # 第一个参数放进%eax
addl %esi, %eax # 把第二个参数加到%eax,%eax同时也是存放返回值的寄存器
addl -4(%rbp), %eax # 加上c的值
addq $4, %rsp # 缩小栈
# 函数调用的尾声,恢复栈指针为原来的值
popq %rbp # 恢复调用者栈帧的底部数值
retq # 返回
.globl _main # .global伪指令让_main函数外部可见
_main: ## @main
# 函数调用的序曲,设置栈指针
pushq %rbp # 把调用者的栈帧底部地址保存起来
movq %rsp, %rbp # 把调用者的栈帧顶部地址,设置为本栈帧的底部
# 设置第一个和第二个参数,分别为1和2
movl $1, %edi
movl $2, %esi
callq _fun1 # 调用函数
# 为pritf设置参数
leaq L_.str(%rip), %rdi # 第一个参数是字符串的地址
movl %eax, %esi # 第二个参数是前一个参数的返回值
callq _printf # 调用函数
# 设置返回值。这句也常用 xorl %esi, %esi 这样的指令,都是置为零
movl $0, %eax
# 函数调用的尾声,恢复栈指针为原来的值
popq %rbp # 恢复调用者栈帧的底部数值
retq # 返回
# 文本段,保存字符串字面量
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "Hello World! :%d \n"
需要注意,手写的代码跟编译器生成的可能有所不同,但功能是等价的,代码里有详细的注释,你肯定能看明白。
借用这个例子,我们讲一下栈的管理。在示例代码的两个函数里,有这样的固定结构:
确认放弃笔记?
放弃后所记笔记将不保留。
新功能上线,你的历史笔记已初始化为私密笔记,是否一键批量公开?
批量公开的笔记不会为你同步至部落
公开
同步至部落
取消
完成
0/2000
荧光笔
直线
曲线
笔记
复制
AI
  • 深入了解
  • 翻译
    • 英语
    • 中文简体
    • 中文繁体
    • 法语
    • 德语
    • 日语
    • 韩语
    • 俄语
    • 西班牙语
    • 阿拉伯语
  • 解释
  • 总结

这篇文章深入探讨了汇编语言编程与栈帧管理的基础知识,重点介绍了汇编代码编程中的参数传递、栈帧管理、函数调用以及汇编指令操作。通过详细讲解过程调用和栈帧管理的实际场景,并结合示例程序对函数调用时参数传递、栈帧的管理以及局部变量的使用进行了解析。文章还涉及了浮点数的使用,介绍了浮点数传参、浮点数加法运算指令以及浮点数返回值的处理方式。通过对比手写汇编代码和对应的C语言代码,读者可以深入理解汇编语言的编写思路和实现原理。总体而言,本文适合想要深入了解汇编语言编程和栈帧管理的读者,通过实际示例帮助读者加深对汇编语言的理解和掌握。文章内容涵盖了汇编代码编程中的参数传递、栈帧管理、函数调用和汇编指令操作,对于想要深入了解汇编语言编程的读者具有很高的参考价值。

仅可试看部分内容,如需阅读全部内容,请付费购买文章所属专栏
《编译原理之美》
新⼈⾸单¥59
立即购买
登录 后留言

全部留言(18)

  • 最新
  • 精选
  • 骨汤鸡蛋面
    老师,通过今天学习有以下总结: 1. pushq 和 popq 虽然是单“参数”指令,但一个隐藏的“参数”就是 %rsp。 2. 通过移动 %rsp 指针来改变帧的大小。%rbp 和 %rsp 之间的空间就是当前栈帧。 3. 栈帧先进后出 (一个函数的相关 信息占用一帧)。或者栈帧二字 重点在帧上。%rbp 在函数调用时一次移动 一个栈帧的大小,**%rbp在整个函数执行期间是不变的**。 4. 函数内部访问 栈帧 可以使用 `-4(%rbp)`表示地址,表示%rbp 寄存器存储的地址减去4的地址。说白了,**栈帧内可以基于 (%rbp) 随机访问**,`+4(%rsp)`效果类似。 5. **%rsp并不总是指向真实的栈顶**:在 X86-64 架构下,新的规范让程序可以访问栈顶之外 128 字节的内存,所以,我们甚至不需要通过改变 %rsp 来分配栈空间,而是直接用栈顶之外的空间。比如栈帧大小是16,即·`(%rbp)-(%rsp) = 16`,可以在代码中直接使用 内存地址`-32(%rbp)`。但如果函数内 还会调用 其它函数,为了pushq/popq 指令的正确性,编译器会为%rsp 设置正确的值使其 指向栈顶。 6. 除了callq/pushq/popq/retq 指令操作%rsp外,函数执行期间,可以mov (%rsp)使其指向栈顶一步到位,(%rsp)也可以和(%rbp)挨着一步不动,也可以随着变量的分配慢慢移动。

    作者回复: 你总结得很细,很清晰。都可以画出一张脑图了! 这些技术细节,可以找到相应的技术规格文档去阅读,并获得更多你感兴趣的内容。课程里讲的,主要是Unix家族的系统约定。Windows系统可能会不同。但你总能找到相应的技术文档来找到这些约定。 进一步,你可以再寻找下面几个问题的答案: 1.为什么只要移动栈指针就能分配内存。应用程序的内存都是操作系统虚拟出来的,操作系统必须在你使用内存的时候,给你准备好真实的物理内存。对这个问题的回答就涉及一点操作系统的知识,我相信你能查到。 2.在栈里申请内存,相比在堆里相比内存,哪个更快?哪个容易导致内存碎片?哪个更有利于局部性?这两种内存申请方式各自的使用场景是什么?像Python这样的动态语言有可能使用栈吗?像Java这样的语言,在new一个对象的时候,有可能使用栈吗? 希望你能在寻找这些问题答案的时候,获得更多的收获。 或者,可能我会在第二季多少涉及这些内容。

    2020-02-22
    3
    6
  • zKerry
    栈的扩大和缩小有点反直觉啊,为啥扩大是减,缩小是加

    作者回复: 因为栈是从高地址向低地址延伸的。所以地址减的话,才是栈的增长。

    2019-11-04
    6
  • 初心丶可曾記
    图中的%rbp应该是指向【上一帧的%rbp的值】的下方红线部位,不应该是【返回地址】的下方红线

    作者回复: 回头我更新一版图,让图中的箭头指向格子而不是线,这样更加没有歧义。

    2019-10-18
    2
    5
  • ifelse
    不错,有收获

    作者回复: 昵称很赞!

    2021-10-20
    2
    1
  • favoorr
    这个第一次学的时候还真很难一次明白,最好是用 GDB 来单步,观察寄存器的值,一边单步,一边拿自己小本本记,来加深理解

    作者回复: 你讲的很对。最好善用GDB、LLDB这些调试工具,这样很多抽象的知识就变得可视化了!

    2020-12-09
    1
  • 骨汤鸡蛋面
    有个疑惑点:函数调用返回时,一个函数的栈帧是作为一整个单位被丢弃掉嘛?

    作者回复: 对的。栈顶指针重新赋值了,栈顶外的内存就抛弃了。这就是一种很自动的内存管理机制,比在堆里申请和释放内存要简单。 所以说在栈里声明的本地变量,它的生存期跟作用域是一致的(闭包除外)。

    2020-02-20
    1
  • 局部变量的访问,既可以用rbp-的方式,也可以用rsp+的方式,文中实例里,都是rbp-的方式,所以需要管理好rbp这个寄存器。 如果采用rsp+的方式,是不是根本就不需要rbp这个寄存器了,这样效率不就更高了? 我看到的一些ARM核,里面只有rsp寄存器,没有rbp寄存器,这样是不是更好呢?

    作者回复: 没错。用两个寄存器来标记栈桢,确实有点浪费。实际上是可以优化掉的。 如果你用gcc编译的话,可以使用-fomit-frame-pointer参数来生成汇编,会把下面三行代码都去掉。 pushq %rbp movq %rsp, %rbp popq %rbp 我在34讲的一个例子中,手工去掉了这三行代码,生成的机器码可以少5个字节,还少两次内存访问,其中有一次是写操作,高速缓存都帮不上忙。对于追求极致性能的程序来说,这个优化是必要的。

    2019-11-05
    1
  • 阿鼎
    协程的切换,用户态代码要复制堆栈寄存器信息。也想请教老师,协程调度是否只能在io线程呢?非io线程能否用协程呢?

    作者回复: 非io当然可以用协程。比如迭代器、状态机用协程来写就很优雅。

    2019-10-18
    3
    1
  • 沉淀的梦想
    老师在课中讲了不少“栈”的操作,那编程语言对于"堆"又是用什么指令操作的呢?

    作者回复: 鼓励你用c语言,使用malloc和free来申请和释放内存,看看生成的汇编是怎样的。

    2019-10-14
    3
    1
  • pebble
    例一的俩栈帧图里,rbp跟rsp,是否应该都指向再下一个位置呢,rsp指向的,应该是下次要保存数据的位置吧

    作者回复: 不是。 rbp,指向栈底。这个值在整合函数执行期间是不变的。 rsp,指向栈顶。这个值会在某些情况下改变: (1)push和pop命令可以改变rsp。 (2)call指令,因为要把返回地址压栈,实际也改变了rsp。 (3)在使用本地变量时,手工改变rsp的值。 rsp如果指向下次要保存数据的位置,相当于栈里总有一个空单元。

    2019-10-14
    2
    1
收起评论
显示
设置
留言
18
收藏
沉浸
阅读
分享
手机端
快捷键
回顶部