by 张悠慧教授(清华大学),课程链接 https://www.bilibili.com/video/av27895807/?p=1 ,大概有十几个小时的视频。看完课程之后我又回看了阮一峰老师的《汇编语言入门》博客 http://www./blog/2018/01/assembly-language-primer.html 。因此本笔记就依据这两份资料来总结编写。 另外,我觉得学习汇编语言之前最好先了解 计算机组成 的相关知识,否则遇到一些 前言学完 计算机组成原理 之后接下来再学什么?通过本课程一开始的图,就知道要紧接着学习汇编语言(再往下是编译原理、操作系统)。 本课程内容太多我没有看完,大概看了 2/3 吧,但这并不影响我来做这个总结记录,因为我不是专业搞汇编的,就来了解一下。
PS:虽然本课程十几个小时,看着不长,但是老师语速非常快,1x 速度听都感觉讲话挺快的,因此不敢 1.5x 看。 学到了什么这个问题其实可以拆解成两个问题:第一,汇编语言是什么?第二,高级语言程序猿学习汇编有何用?分开解答。 汇编语言是什么简答来说,汇编语言就是机器语言(二进制代码)的助记符,每条汇编语言都能直接翻译成机器语言,如下图。计算机就是一台各种电子设备组成的机器,它只能识别机器代码,即一堆二进制数字。但是二进制不易于人类阅读,而且在计算机发展初期还没有高级语言和编译器,因此出现了汇编语言。仅仅这样一个微创新,就大大提升了开发效率。 汇编语言常见的语法是 汇编语言都是针对特定的计算机体系结构的,例如 x86 汇编(本课重点内容)、MIPS 汇编、ARM 汇编,因此没有让所有计算机都通用的汇编语言。 高级语言的开发者,学习汇编有何用一句话总结就是:了解程序是在计算机中是如何被执行的,即透过现象(高级语言)看本质 —— 这是所有领域的技术人员都应该追求的东西。那些能随意在 php java js C++ 等语言中随意切换的程序猿大牛,我想他肯定熟知这个本质。 无论你日常编写的语言多么高级,肯定最终经过转换(编译原理的内容)然后生成汇编语言这种最底层的语言,再被计算机执行。而“执行”的本质,就可以通过汇编语言的一行一行代码看出:使用了哪个指令、获取了哪个内存地址、操作了哪个内存片段或者寄存器…… 另外一个重要的部分就是程序执行的时候的内存模型。一段程序拿过来,哪些变量将被放在栈 stack ,哪些变量将被放在堆 heap ?以及这些内存空间如何被释放?甚至是你日常遇到的爆栈、内存泄露等问题,了解了内存模型,这些都会变的非常具象,不再懵。 指令集分类所谓“指令集”,我理解就是一套操作 CPU 的指令体系集合,以及体系规范。指令集是一种上层定义,汇编就是其具体的体现和实现。指令集分两类:
CISC最初的计算机编程很麻烦,例如用纸带打孔输入,因此计算机的设计者就考虑将 CPU 做的复杂一点,以简化这种本来就很麻烦的编程。因此有了 CISC 复杂指令集。x86 就是其中的典型代表,x86 的特点是:
RISC历史原因,RISC 是 80 年代初发明的,那时整个计算机生态系统已经形成,编译器能力增强,就不需要 CPU 对外暴露过度复杂的指令集,因此有了 RISC 精简指令集。MIPS ARM 是 RISC 的代表,RISC 指令集特点是:
MIPS 特点:
ARM 指令集特点:
两者对比。现代计算机中,像 x86 结构虽然也是 CISC ,但那时对外的,内部实现还是类似 RISC 实现的。因此,随着历史发展 CISC 和 RISC 的界限也越来越模糊。如果非要区分两者,可以看是不是只允许 数字的二进制表示数字用二进制表示终归是一个数学问题,而常用的文本(中文、英文等)如何用二进制表示,这就是“编码”领域的问题。 普通整数例如十进制 3 的二进制表示是 11 这没问题,但是在计算机中表示也是 11 吗?—— 不对,得分情况。例如在 C 语言中:
负数补码 二进制负数是通过补码来表示的,补码算法是:按位取反、末尾加 1 。为何要用补码呢?建议读者看下阮一峰老师的《关于2的补码》 http://www./blog/2009/08/twos_complement.html ,里面讲的比我这里详细。下面简单通过一个例子来说明:
有符号数和无符号数 计算机肯定是看不懂正数、负数的,它只能识别二进制数字。那么计算机如何知道一个数是正数还是负数呢?要看两点:
因此 C 语言中的数字类型就有很多种,适用于不同长度。而每种数字类型,又分有符号性和无符号型。即便是是 PS:日常开发中,尽量别用无符号数,会带来运算问题。C 语言中,有符号数和无符号数一起进行算数运算是,会将有符号数转换为无符号数(负数第一 bit 的 其他 除法计算比较复杂,如果遇到以 2 为底数的除法,尽量使用位运算。例如 js 中的 浮点数浮点数的二进制表示比较复杂,细节部分可以忽略 十进制小数如何转换为二进制小数 规则是:整体规则是“乘 2 取整,顺序排列”,例如:
因此,二进制能精确表示的小数,只能是若干次 浮点数的二进制存储 IEEE (美国电器与电子工程师协会)的浮点数标准参考一下 http://www./blog/2010/06/ieee_floating-point_representation.html,即将一个存储空间分成三段:
通过以上几个区域能计算出它存储的浮点数的数值,按公式
整数和浮点数的转换
x86 计算机系统结构我感觉这部分算是对计算机组成原理的一个简单介绍,但我更加推荐大家去专门的计算机组成原理的课程去详细学习。 计算机系统结构模型主要结构分为:
x86 的保护模式8086 是 intel 在 1978 年发布的 16 位处理器,80386 是 1985 年 intel 发布的 32 位处理器(寄存器 32 位)。80386 有三种工作模式:
有了保护模式,编程人员才可以在一个私有的空间内为所欲为。 和汇编程序相关的结构就好像程序猿占有了一个(虚拟的) CPU 和一段内存地址
这部分内容中,寄存器的知识对于汇编语言是很重要的,阮一峰老师的博客中也介绍了寄存器,大家可以去参考。 程序执行时的内存模型汇编语言是面向机器的最基础编程,既然是编程就涉及到内存的使用和分配,于是就有了内存模型。这部分的知识,我感觉阮一峰老师的博客中已经写的很详细了,我也会参考他的文章来进行下文的总结。 分配内存空间某个程序开始运行之前,操作系统会给它分配一段内存空间,用于存储改程序时使用的、产出的数据。具体这块内存区域的大小和起止指针先不用关心。 栈 Stack栈这个数据结构的特点是“先进后出”。像 C 语言这种“过程调用过程”后者“函数调用函数”的执行方式,最先调用的过程或者函数,会是最后一个结束。这一特点和栈的特点基本一致。 需要强调一点,在整个这段内存空间中,栈是自上(高地址)而下(低地址)进行累积的,即栈顶的内存地址比栈底的内存地址要小。这一点和堆正好相反,如下图: 压栈 push 当一个过程或者函数被执行时,会有一些数据(参数、局部变量、返回地址)需要临时存储起来。而且在“函数调用函数”的整个过程中,会有很多这样的操作。那么就在每个函数执行时,将这些数据压栈。如下图,注意调用链和压栈的关系(其中两个 当前正在执行的函数对应的栈,叫做“栈帧”, PS:递归和循环虽然都可以满足某些计算场景,但是在构建内存模型上是完全不一样的,递归复杂度更高。 出栈 pop 栈中的数据是有声明周期的,每个函数执行完 return 之后,其对应的数据就要被 pop ,并释放这段内存空间。因此栈的内存空间是由系统分配、系统自动释放,不需要人为干预。人只管好好写自己的程序就 OK 了。 可以拿上图中的调用链和栈写一个详细的调用过程:
其他 有一个程序猿知名网站叫 stackoverflow ,意思就是“栈溢出”。按照上述模型的理解,就是程序执行时栈内存累计过多,导致溢出了整个分配的内存空间了。常见的导致这种问题的方式是大量的递归调用,可以用“尾递归”来解决这一问题,感兴趣的可以去具体查一查。 堆 heap在整个程序被分配的内存空间里,栈是系统自己使用和分配,自上而下的累积。其中还有一部分内存空间是给程序猿使用的,即你可以通过程序动态占有一部分内存(如 C 语言的
常说的内存泄露就是在堆中占有的内存没有被及时的清理或者 GC ,导致长时间积累之后内存崩溃。对于 JS 开发者,应该知道 Chrome devtools 中有一个 heap Snapshot ,用来记录当前时刻 JS 堆内存,如下图: 常见数据结构的存储方式以 C 语言中的数组和结构体为例。 C 语言中,数组需要一个连续的存储空间,每个数组需要一个 PS:数组和链表有时候看着用途一样,但是数据结构上是有明显区别的,链表不需要一个连续的存储空间。 C 语言结构体也需要一个连续的存储空间,结构提内部通过名字访问,每个元素都可以有不同的类型。
以上代码将会被分配这样一个连续的内存地址: 简单的汇编指令虽然本课是主讲汇编语言,课程中也花了大量的时间讲解了常用的指令、示例以及 C 语言和汇编语言的如何对应。不过对于我这种以了解汇编、学习基础知识为目的的高级语言的开发者,并没有去认真听每个指令的具体意义。不知道这是不是常说的“不求甚解”。 简单指令课程中几个比较简单的汇编指令如下:(阮一峰老师的博客中也讲了一些常用指令,讲的更加详细,可以去学习)
上述两个指令, 参数中, 条件码和分支上文中【和汇编程序相关的结构】图中可以看到,CPU 中有“条件码”。例如,x86 中常用的四个条件码(其实我也不知道怎么用……)
(每个条件码只占 1 bit 空间,可见它是一个 boolean 型的存在) 在指令运行过程中,硬件会根据指令运行的状态实时的修改这些条件码的值,然后用 跳转以 执行逻辑验证高级编程语言中有三种基本的执行逻辑:第一,顺序执行,这个对应汇编语言没啥问题;第二,分支执行(即 if else);第三,循环执行。后两种,通过判断条件码和跳转也都可以实现。 关于递归,课程中也讲了很多内容,不过我没看懂(没有那么那么认真的看,看不懂就算了……)。 程序示例如果想简单看一下汇编语言是什么样子的,可以通过 gcc 编译一段简单的 C 语言来看下。首先,新建一个
在该文件目录中运行
这就是 C 语言编译出来的汇编语言。具体的示例,可以去看阮一峰老师那篇博客最后的内容,他在博客中对一段汇编语言做了详细的解释。我这里就省略了。 最后仅仅是一个学习笔记,发现错误欢迎指正。 |
|