前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >计算机底层知识之汇编语言

计算机底层知识之汇编语言

作者头像
前端柒八九
发布2022-12-19 21:38:52
4110
发布2022-12-19 21:38:52
举报
文章被收录于专栏:柒八九技术收纳盒

❝大家往往高估自己一天能学会的东西,低估三年能学会的东西 ❞

大家好,我是「柒八九」

今天,我们继续「计算机底层知识」的探索。我们来谈谈关于「汇编语言」的相关知识点。

如果,想了解该系列的文章,可以参考我们已经发布的文章。如下是往期文章。

文章list

  1. 计算机底层知识之CPU
  2. 计算机底层知识之二进制
  3. 计算机底层知识之处理小数
  4. 计算机底层知识之内存和磁盘的关系&数据压缩
  5. 计算机底层知识之运行环境&可执行文件
  6. 计算机底层知识之操作系统

你能所学到的知识点

  1. 汇编语言和本地代码是「一一对应」「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  2. 不会转换成本地代码的伪指令 「推荐阅读指数」 ⭐️⭐️⭐️
  3. 汇编语言的语法是「操作码 + 操作数」 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  4. mov指令 「推荐阅读指数」 ⭐️⭐️⭐️
  5. 对栈进行push 和 pop 「推荐阅读指数」 ⭐️⭐️⭐️
  6. 函数调用机制 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  7. 函数内部的处理 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️
  8. 全局变量用的内存空间 「推荐阅读指数」 ⭐️⭐️⭐️
  9. 循环处理的实现方法 「推荐阅读指数」 ⭐️⭐️⭐️⭐️⭐️

好了,天不早了,干点正事哇。


汇编语言和本地代码是一一对应的

在前面的文章中我们多次提到,计算机CPU能直接解释运行的只有「本地代码」(机器语言)程序。用C语言等编写的源代码,需要通过各自的「编译器」编译后,转换成本地代码。

通过调用本地代码的内容,可以了解程序最终是以何种形式来运行的。但是,直接打开本地代码来看,只能看到数值的罗列。

我们可以采用另外一种方式,在各本地代码中,附带上表示其功能的英语单词缩写。例如,在加法运算的本地代码中加上add、在比较运算的本地代码中加上cmp等。这些缩写被称为「助记符」,使用助记符的编程语言称为「汇编语言」

不过,「即使是用汇编语言编写的源代码,最终也必须要转换成本地代码才能运行」。负责准换工作的程序称为「汇编器」,转换这个一处理本身称为「汇编」

❝用汇编语言编写的源代码,和本地代码是一一对应的 ❞

本地代码也可以反过来转换成汇编语言的源代码。持有该功能的「逆变换」程序称为「反汇编程序」,逆变换这一处理本身称为「反汇编」


不会转换成本地代码的伪指令

汇编语言的源代码,是由转换本地代码的指令和针对汇编器的「伪指令」构成的。「伪指令负责把程序的构造及汇编的方法指示给汇编器(转换程序)」。不过,伪指令是无法汇编转换成本地代码。

如上是一个汇编代码片段。其中「彩色」部分是伪指令。

由伪指令segmentends围起来的部分,是给构成程序的命令和数据的集合体加上一个名字而得到的,称为「段定义」。段定义的英文表达segment具有区域的意思。在程序中,「段定义指的是命令和数据等程序的集合体的意思」

❝一个程序由多个段定义构成 ❞

如上图所示。源代码的开始位置,定义了3个名称分别为_TEXT_DATA_BSS的段定义。

  • _TEXT是指令的段定义
  • _DATA是被初始化(有初始值)的数据的段定义
  • _BSS是尚未初始化的数据的段定义

而这些段定义的名称及划分方法,不同的编译器都有自己的一套规则。

伪指令procendp围起来的部分,表示的是过程Proceduce的范围。在汇编语言中,这种相当于C语言的函数的形式称为过程。


汇编语言的语法是「操作码 + 操作数」

「在汇编语言中,1行表示对CPU的一个指令」。汇编语言指令的语法结构是「操作码」+「操作数」

  • 「操作码」表示的是指令动作
  • 「操作数」表示的是指令对象

操作码和操作数罗列在一起的语法,就是一个英文的指令文本。操作码是动词,操作数相当于宾语。

能够使用何种形式的操作码,是由CPU的种类决定的。

常用操作码的功能

本地代码加载到内存后才能运行。内存中存储着构成本地代码的指令和数据。程序运行时,CPU会从内存中把指令和数据读出,然后再将存储在CPU内部的寄存器中进行处理。

「寄存器是CPU中的存储区域」。不过,寄存器并不仅仅具有存储指令和数据的功能,也有运算功能。寄存器的名称会通过汇编语言的源代码指定给操作数。内存中的存储区域是用「地址编号」来区分的。CPU内的寄存器是用eaxebx这些名称开区分的。

下图是CPU的寄存器的主要种类和角色


mov指令

mov指令的两个操作数,分别是用来指定数据的「存储地」「读出源」

操作数可以指定寄存器、常数、标签(附近在地址前)以及用方括号([])围起来的这些内容。

  • 如果指定了「没有用方括号围起来」的内容,就表示对该值进行处理
  • 如果指定了「用方括号围起来」的内容,方括号中的值则会被解释为「内存地址」,然后就会对该内存地址对应的值进行读写操作
代码语言:javascript
复制
mov ebp,esp;
mov eax,dword ptr [ebp+8];

mov ebp,esp中,esp寄存器中的值被直接存储在ebp寄存器中。esp寄存器的值是100ebp寄存器的值也是100

mov eax,dword ptr [ebp+8];中,ebp寄存器的值加8后得到的值会被解释为内存地址。如果ebp寄存器的值是100的话,那么eax寄存器中存储的就是100 + 8 = 108地址的数据。


对栈进行push 和 pop

❝程序运行时,会在内存上申请分配一个称为「栈」的数据空间。 ❞

在栈中,数据在存储时是从内存的下层(大的地址编号)逐渐往上层(小的地址编号)累积,读出时则是按照从上往下的顺序进行。

栈是「存储临时数据的区域」,它的特点是通过push指令和pop指令进行数据的存储和读出。push指令和pop指令中只有一个操作数。该操作数表示的是「push的是什么及pop的是什么」,而不需要指定”对哪一个地址编号的内存进行pushpop“。

这是因为,对栈进行读写的内存地址是有esp寄存器(栈指针)进行管理的。push指令和pop指令运行后,esp寄存器的值会「自动进行更新」push指令是-4,pop指令是+4),因而就没有必要指定内存地址了。


函数调用机制

假设存在如下的C语言代码片段。

代码语言:javascript
复制
// 返回两个参数值之和的函数
int  AddNum(int a,int b){
  return a + b;
}

// 调用AddNum函数的函数
void MyFunc(){
  int c;
  c = AddNum(123,456);
}

转换成对应的汇编语言的代码如下。

这里我们先介绍(3)~(6)的部分,这对了解函数调用的机制很重要。

(3)(4)表示的是将传递给AddNum函数的参数通过push入栈。在C语言中,虽然记述为函数AddNum(123,456),但入栈的则会按照456123这样的顺序,也就是位于「后面的数值先入栈」

(5)call指令,把程序流程跳转到了操作数中指定的AddNum函数所在的内存地址处。在汇编语言中,「函数名表示的是函数所在的内存地址」AddNum函数处理完毕后,程序流程必须要返回到编号(6)这一行。call指令运行后,call指令的下一行((6)这一行)的内存地址会「自动」push入栈。该值会在AddNum函数处理的最后通过ret指令pop出栈,然后程序流程就会返回到(6)这一行。

(6)部分会把栈中存储的两个参数(456123)进行销毁处理,也就是「栈清理处理」。虽然通过使用两次pop指令也可以实现,不过「采用esp寄存器加8的方式更有效率」(处理一次)。对栈进行数值的输入输出时,数值的单位是4字节。因此,通过在栈地址管理的esp寄存器加上4的2倍8,就可以达到和运行两次pop命令同样的效果。

AddNum函数调用前后栈的状态变化


函数内部的处理

继续分析执行AddNum函数的源代码部分。

ebp寄存器的值在(1)中入栈,在(5)中出栈。这主要是为了把函数中用到的ebp寄存器的内容,恢复到函数调用前的状态。CPU拥有的寄存器是有数量的限制的。在函数调用前,调用源有可能已经在使用ebp寄存器了。因而,「在函数内部用的寄存器,要尽量返回到函数调用前的状态」

(2)中负责管理栈地址的esp寄存器的值赋值到了ebp寄存器中。这是因为,在mov指令中方括号内的参数,是不允许指定esp寄存器的。因此,这里就采用了不直接通过esp,而是用ebp寄存器来读写栈内容的方法。

(3)是用[ebp+8]指定栈中存储的第1个参数123,并将其读出到eax寄存器中。eax寄存器是负责运算的累加寄存器

通过(4)add指令,把当前eax寄存器的值同第2个参数相加后的结果存储在eax寄存器中。「函数的参数是通过栈来传递,返回值是通过寄存器来返回的」

(6)ret指令运行后,函数返回目的地的内存地址会自动出栈。

AddNum函数内部的栈状态变化


全局变量用的内存空间

在一些高级编程语言中,在函数外部定义的变量称为「全局变量」,在函数内部定义的变量称为「局部变量」。全局变量可以在源代码的任意部分被引用,而局部变量只能在定义该变量的函数内进行引用。

高级程序语言被编译后,会被归类到名为「段」定义的组。

  • 初始化的全局变量被汇总到名为_DATA的段定义中
  • 没有初始化的全局变量被汇总到名为_BSS的段定义中
  • 指令被汇总到名为_TEXT的段定义中

局部变量的内存空间

「局部变量只能在定义该变量的函数内进行引用」,这是因为,局部变量是临时保存在寄存器和栈中的。

函数内部利用的栈,在函数处理完毕后会恢复到初始状态,因此局部变量的值也就会被销毁,而寄存器也可能被用于其他目的。因此,局部变量只是在函数处理运行期间临时存储在寄存器和栈上。

用于局部变量的栈空间的申请分配和释放


循环处理的实现方法

假设我们存在如下的代码,将局部变量i作为循环计数器连续进行10次循环的C语言源代码。

代码语言:javascript
复制
// 定义MySub函数
void MySub(){
  // 省略部分处理
}
// 定义MyFunc函数
void MyFunc(){
  int i;
  for(i=0;i<10;i++){
    // 重复调用MySub函数10次
    MySub();
  }
}

将上述的代码转换成汇编语言如下(仅展示for片段)

C语言for语句是通过在括号中指定「循环计数器」的初始值(i=0)、循环的继续条件(i<10)、循环计数器的更新(i++)这3种形式来进行循环处理。与此相对,

❝在汇编语言的源代码中,循环是通过「比较指令」cmp)和「跳转指令」jl)来实现。 ❞

具体流程我们就不在这里赘述。这里挑选比较重要的点来分析下。

cmp指令是用来对第一个操作数和第二个操作数的数值进行比较的指令。cmp ebx,10就相当于C语言i<10这一处理,意思是把ebx寄存器的数值同10进行比较。汇编语言中比较指令的结果,会存储在CPU「标志寄存器」中。

最后一行的jljump on less than(小于的话就跳转)的意思。也就是说,jl short @4的意思就是,前面运行的比较指令的结果,若「小」的话就跳转到@4这个「标签」


条件分支的实现方式

条件分支的实现方法同循环的实现方法类似,使用的也是cmp指令和跳转指令。


后记

「分享是一种态度」

参考资料:《程序是怎样跑起来的》

「全文完,既然看到这里了,如果觉得不错,随手点个赞和“在看”吧。」

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2022-11-06,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 前端柒八九 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 文章list
  • 你能所学到的知识点
  • 汇编语言和本地代码是一一对应的
  • 不会转换成本地代码的伪指令
  • 汇编语言的语法是「操作码 + 操作数」
  • mov指令
  • 对栈进行push 和 pop
  • 函数调用机制
  • 函数内部的处理
  • 全局变量用的内存空间
  • 局部变量的内存空间
  • 循环处理的实现方法
  • 条件分支的实现方式
  • 后记
相关产品与服务
对象存储
对象存储(Cloud Object Storage,COS)是由腾讯云推出的无目录层次结构、无数据格式限制,可容纳海量数据且支持 HTTP/HTTPS 协议访问的分布式存储服务。腾讯云 COS 的存储桶空间无容量上限,无需分区管理,适用于 CDN 数据分发、数据万象处理或大数据计算与分析的数据湖等多种场景。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档