2019-2020-1 20199313《Linux内核原理与分析》第二周作业
第二周学习:老友相聚——汇编语言与堆栈
Review && Preview
- 继上周初步认识并学习了Linux的操作界面,本次学习将深入计算机的工作原理,了解冯·诺依曼体系结构和基本的汇编语言。
- 学了那么久的C语言,那么C语言经由汇编程序汇编之后的代码又将如何在存储程序计算机工作模型上逐步运行?
- C语言的堆栈模型真是个默认的小妖精,各种寄存器和指针又是如何相互配合,完美的形成堆栈呢?
以存储为核心的冯·诺依曼体系
简而言之,存储器才是核心。
体系结构
- 冯诺依曼体系结构如上图所示,硬件大类应当包括运算器、存储器、控制器、输入输出设备
- 计算机内内部采用二进制来表示指令和数据,这些指令和数据将会在CPU中逐条运行。
x86-32CPU的寄存器
学过了微机原理与接口技术,相比了解到计算机详细运行过程中寄存器的目录清单就和汇编语言指令表一样重要:
(仅有指令表却不知道该操作那个寄存器的情况,就如同完全不知道指令表一样糟糕):
寄存器
(这里仅记录本节课比较需要了解或者用到的,其余用到时再阐述)
AX、BX、CX和DX统称为数据寄存器
- AX:(accumulator)累加器
- BX:(Base)基址寄存器
- CX:(Count)计数器寄存器
- DX:(Data)数据寄存器
SP、BP统称为指针寄存器
-
SP:(Stack Pointer)堆栈指针寄存器
(与SS配合使用,用于指向目前的堆栈位置) -
BP:(Base Pointer)基址指针寄存器
(区别于BX,在基址寻址过程中BX用于存放基地址,而BP用于存放偏移量)
以及堆栈中较为常用而又不好区分的几个指针寄存器
三个指针寄存器
-
ESP(Extended Stack Pointer):扩展栈指针寄存器(用于存放当前线程栈顶指针)
-
EBP(Extended Base Pointer):扩展基址指针寄存器(用于存放当前线程栈底指针)
-
EIP(Extended Instruction Pointer):指令指针寄存器(用于存放下一条将要被执行的指令的地址)
需要注意的是,EIP这个寄存器不能人工被直接修改,否则容易引起安全隐患(例如mov &100, eip 这样的指令是被禁止的)
堆栈过程
在最简单的程序运行过程中,指令只是一条接着一条运行到底。但是,当遇到一些情况下,需要暂时停止运行主干程序,而需要调用某个函数进行一定的运算时,当前各个寄存器中的数据又十分重要不能随便丢弃,需要放入存储器暂时储存,当再次需要用到时也需要快速调出,于是,栈便应运而生。
其实和单片机的堆栈大体过程并无不同,只不过需要注意的是各条指令所带来的细微的不同差别。
(关于如何将C语言汇编为汇编语言的详细步骤我们不再详细赘述)
栈的示意图
栈同样是寄存器的一部分,他的大致示意图如下:
汇编指令
在汇编语言中,以字符'.'开头的指令行视为备注,计算机不对其进行处理
(例如: .file "main.c" 语句的意思为名称为main类型为.c的文件)
(据以往经验来看,对于每一款单片机都有属于自己的汇编指令表,但都具有共通点)
这里举例详解几个汇编指令,对理解栈的过程提供帮助:
movl 指令
- 作为赋值指令,详细为 movl a ,b 即为将a的值赋值给b,等同于C语言中的b=a
相应的也会有不同的寻址方式,曾经学过,有一篇博客阐述甚为清晰,这里不再赘述,详情参照《汇编语言之寻址方式》
call 指令
-
函数调用指令
对于程序员而言,我们在C语言经常用到此类操作,而这条指令也可以被形象的转换为两条伪指令:
pushl %eip(*) movl $0x1234, %eip(*)
- 即:将下一条将要运行的指令的地址存入堆栈,并将被调用函数的首指令的地址放入指针寄存器(在真正编写汇编语言时,不能直接对%eip进行赋值)
ret 指令
-
函数返回指令
在被调用的函数中使用,返回至调用前的位置,其伪指令如下:
popl %eip(*)
即:将之前指令指针寄存器入栈的值重新取出,并赋值给指令指针寄存器,让指令从之前断掉的地方继续开始运行。
pushl 指令
-
入栈指令
将某一数据压入堆栈,例如 pushl %eas 形象地表示为:
subl $4, $esp movl %eax, (%esp)
将栈顶指针移向下一个空位,并将%eax中存放的数据压入堆栈
popl 指令
-
出栈指令
将堆栈中位于最顶部的数据进行出栈操作,例如 popl %eax 形象的表示为:
movl (%esp), %eax add $4, $esp
-
将位于栈顶的数据赋值给%eax,并将栈顶指针移向上一个位置。
其中比较特殊的是:
pushl %ebp
movl %esp, ebp
sub $4, %esp
- 这样的操作指令,可以理解为建立了一个新的栈,将之前的栈底指针入栈,并将栈顶指针移向下一个空位,一般配合 call 指令使用,在调用新的函数时为其建立一个新的栈。
实验一的结论在参照上述知识点的情况下便很容易理解了,具体的执行过程如下图所示:
具体在Linux系统中的操作过程如下:
- 在终端中创建 main.c 文件
- 并使用gcc命令将其编译为汇编语言,详细汇编语言如下图示
- 如此多的备注,该如何删除这些字段,参考了博客《vi/vim如何添加或删除多行注释》中所述:
可以用鼠标拖动光标(或者使用Ctrl+V)进入可视化窗口,手动选中要删除的行,按下键盘上的D键,删除所选行,如图
也可以使用替换命令或者删除命令,例如 g/.s*/d 进行统一删除
最终结果,清晰明了