编译原理之美
宫文学
北京物演科技CEO
立即订阅
8171 人已学习
课程目录
已完结 43 讲
0/4登录后,你可以任选4讲全文学习。
开篇词 (1讲)
开篇词 | 为什么你要学习编译原理?
免费
实现一门脚本语言 · 原理篇 (13讲)
01 | 理解代码:编译器的前端技术
02 | 正则文法和有限自动机:纯手工打造词法分析器
03 | 语法分析(一):纯手工打造公式计算器
04 | 语法分析(二):解决二元表达式中的难点
05 | 语法分析(三):实现一门简单的脚本语言
06 | 编译器前端工具(一):用Antlr生成词法、语法分析器
07 | 编译器前端工具(二):用Antlr重构脚本语言
08 | 作用域和生存期:实现块作用域和函数
09 | 面向对象:实现数据和方法的封装
10 | 闭包: 理解了原理,它就不反直觉了
11 | 语义分析(上):如何建立一个完善的类型系统?
12 | 语义分析(下):如何做上下文相关情况的处理?
13 | 继承和多态:面向对象运行期的动态特性
实现一门脚本语言 · 应用篇 (2讲)
14 | 前端技术应用(一):如何透明地支持数据库分库分表?
15 | 前端技术应用(二):如何设计一个报表工具?
实现一门脚本语言 · 算法篇 (3讲)
16 | NFA和DFA:如何自己实现一个正则表达式工具?
17 | First和Follow集合:用LL算法推演一个实例
18 | 移进和规约:用LR算法推演一个实例
实现一门脚本语言 · 热点答疑与用户故事 (2讲)
19 | 案例总结与热点问题答疑:对于左递归的语法,为什么我的推导不是左递归的?
用户故事 | 因为热爱,所以坚持
编译原理 · 期中考试周 (1讲)
期中考试 | 来赴一场100分的约定吧!
免费
实现一门编译型语言 · 原理篇 (12讲)
20 | 高效运行:编译器的后端技术
21 | 运行时机制:突破现象看本质,透过语法看运行时
22 | 生成汇编代码(一):汇编语言其实不难学
加餐 | 汇编代码编程与栈帧管理
23 | 生成汇编代码(二):把脚本编译成可执行文件
24 | 中间代码:兼容不同的语言和硬件
25 | 后端技术的重用:LLVM不仅仅让你高效
26 | 生成IR:实现静态编译的语言
27 | 代码优化:为什么你的代码比他的更高效?
28 | 数据流分析:你写的程序,它更懂
29 | 目标代码的生成和优化(一):如何适应各种硬件架构?
30 | 目标代码的生成和优化(二):如何适应各种硬件架构?
实现一门编译型语言 · 应用篇 (2讲)
31 | 内存计算:对海量数据做计算,到底可以有多快?
32 | 字节码生成:为什么Spring技术很强大?
实现一门编译型语言 · 扩展篇 (3讲)
33 | 垃圾收集:能否不停下整个世界?
34 | 运行时优化:即时编译的原理和作用
35 | 案例总结与热点问题答疑:后端部分真的比前端部分难吗?
面向未来的编程语言 (3讲)
36 | 当前技术的发展趋势以及其对编译技术的影响
37 | 云编程:云计算会如何改变编程模式?
38 | 元编程:一边写程序,一边写语言
结束语 (1讲)
结束语 | 用程序语言,推动这个世界的演化
编译原理之美
登录|注册

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

宫文学 2019-10-14
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/1000字
划线
笔记
复制
© 版权归极客邦科技所有,未经许可不得传播售卖。 页面已增加防盗追踪,如有侵权极客邦将依法追究其法律责任。
该试读文章来自付费专栏《编译原理之美》,如需阅读全部文章,
请订阅文章所属专栏。
立即订阅
登录 后留言

精选留言(7)

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

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

    2019-10-18
    2
  • zKerry
    栈的扩大和缩小有点反直觉啊,为啥扩大是减,缩小是加

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

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

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

    2019-10-14
    2
    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
  • 阿鼎
    协程的切换,用户态代码要复制堆栈寄存器信息。也想请教老师,协程调度是否只能在io线程呢?非io线程能否用协程呢?

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

    2019-10-18
  • 沉淀的梦想
    老师的as用的什么版本,为什么我用as汇编文稿中的代码会出错(注释删了也一样会出错):

    function-call2-craft.s: Assembler messages:
    function-call2-craft.s:2: Error: no such instruction: `n-call2-craft.s 函数调用和参数传递'
    function-call2-craft.s:4: Error: character following name is not '#'
    function-call2-craft.s:66: Error: character following name is not '#'

    作者回复: macOS操作系统,gnu汇编器。
    Linux上生成的汇编码应该也差不多,有些系统调用是不同的。
    windows上,你就要安装一下相关的环境了和工具了,比如MinGW。或者装一个Linux虚拟机。

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

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

    2019-10-14
    1
收起评论
7
返回
顶部