深入理解操作系统(5)第三章:程序的机器级表示(1)intel历史+程序编码+算术和逻辑操作(包括:8086/汇编/摩尔/机器级代码/汇编指令/objdump/汇编指令/反汇编/8个常用寄存器/lea

1. 前言

1.1 现代编译器的优点

在用高级语言如C语言编程时,我们被屏蔽了程序具体的机器级实现。

1.编译器提供的类型检查能帮助你发现许多程序错误,并能够保证我们是按照一致的方式来引用和管理数据的。
2.使用现代的优化编译器,产生的代码通常至少与一个熟练的汇编语言程序员手工编写的代码一样有效。
3.最好的点就是,用高级语言编写的程序可以在很多不同的机器上编译执行,而汇编代码则是与特定机器密切相关的

1.2 理解汇编代码的重要性

虽然可以使用优化编译器,但是对于严谨的程序员来说,能够阅读和理解汇编代码仍是一项很重要的技能。
与目标代码的二进制格式相比,它的主要特色在于它采用的是更加易读的文本格式。

1.通过阅读这些汇编代码,我们能够理解编译器的优化能力,并分析出代码中潜在的低效率。
2.高级语言提供的抽象层会隐藏我们想要理解的一些信息,如程序的运行时行为。

1.3 编译器的优化

在本章中,我们将学习某种汇编语言的详细内容,明白C程序是如何编译成这种形式的机器代码的。
我们必须了解典型的编译器在将C程序结构变换成机器代码时所做的转换。

相对于C代码中表示的计算操作,优化编译器能够重新排列执行顺序,
消除不必要的计算并替换慢速操作,
1. 例如用加法和移位来代替乘法
2. 甚至于将递归计算变换成迭代计算。

1.4 精通细节是理解更深和更基本概念的先决条件

精通细节是理解更深和更基本概念的先决条件,花点时间研究这些示例并完成练习是非常值得的。

2. 历史观点-Intel处理器发展

2.1 intel历史(8086由来)

8086:(1978,29K个晶体管)。它是第一代单芯片、16位微处理器之一
	8086由来:
		微处理器是在70年代末发明的。接近80年,所以,前两个数字就是这么来的 
		当时微处理器是8位的,因此,第3个数字是8 
		8085升级到16位后,就叫8086了
8088,即8086加上8位外部总线( external bus),构成最初的IBM个人计算机的心脏。
i386:(1985,275K个晶体管)。将体系结构扩展到32位。增加了平面寻址模式( fat addressinmodel),
	Linux和最近版本的 Windows系列操作系统都是使用的这种模式。
Pentium:(1993,3.1M个晶体管)。改善了性能,不过只对指令集增加了小的扩展
Pentium4:(2001,42M个晶体管)。在向量指令中增加了8字节整数和浮点格式,以及针对这些格式的144个新指令

2.2 IA32-Intel32位体系结构

为了保持这种进化传统,指令集中有许多非常奇怪的东西。
Intl现在称其指令集为IA32,也就是“ Intel32位体系结构( Intel Architecture32bit)”。
这个处理器系列也俗称为“x86”,反映出直到ⅰ486的处理器命名惯例。

2.2.1 x86含义

“x86”是指基于Intel 8086且向后兼容的中央处理器指令集架构。
该系列架构处理器较早期的名称是以数字来表示“80x86”,
	由于以“86”作为结尾,包括Intel 8086、80186、80286等,因此其架构被称为“x86”。

目前x86指的就是32位处理器架构

2.3 摩尔定律

1965年, Gordon moore, Intel公司的创始人,根据当时芯片技术,也就是能够在一个芯片上制造有大约64个晶体管的电路,做出推断,预测在未来10年内,每年芯片上的晶体管敷量都会翻番。
这个预测就称为摩尔定律。
半导体工业能够每18个月就将晶体管数目加倍对计算机技术的其他方面,也有类似的呈指数性增长的情况出现,比如磁盘容量,存储器芯片容量,和处理器性能

3. 程序编码

预编译,编译,汇编,链接

预处理器:会扩展源代码,插入所有用#include命令指定的文件,并扩展所有的宏。
編译器:产生源文件的汇编代码
汇编器:会将汇编代码转化成二进制目标代码文件(乱码)
链接器:将目标文件与实现标准Uniⅸ库函数(如printf)的代码合并,并产生最终的可执行文件

https://blog.csdn.net/lqy971966/article/details/105146839

3.1 机器级代码

在整个编译过程中,编译器会完成大部分的工作,将把用C提供的相对比较抽象的执行模型表示的程序转化成处理器执行的非常基本的指令。

汇编代码表示非常接近于机器代码。
与目标代码的二进制格式相比,汇编代码的主要特点是用可读性更好的文本格式表示的。
汇编程序员看到的机器与C程序员看到的机器差别很大。

一些通常对C程序员屏蔽的处理器状态是可见的:

1.程序计数器(称为%eip)表示将要执行的下一条指令在存储器中的地址
2.整数寄存器文件包含8个被命名的位置,分别存储32位的值。(eax/ebx/ecx/edx/esi/edi/esp/ebp)
	这些寄存器可以存储地址(对应于C的指针)或整数数据。
	有的寄存器用来记录某些重要的程序状态,而其他的寄存器用来保存临时数据,例如过程的局部变量。
3.条件码寄存器保存着最近执行的算术指令的状态信息。它们用来实现控制流中的条件变化,
	如:if while 语句

关于寄存器
寄存器(1)寄存器概念,种类说明及汇编代码详解
https://blog.csdn.net/lqy971966/article/details/106780755

3.2 代码示例

3.2.1 代码例子:

code.c

int accum =0;

int sum(int x, int y)
{
int t=x+y;
accum += t;
return t;
}

3.2.2 查看汇编代码:

root@localhost hello]# cat code.s
		.file   "code.c"
		.globl  accum
		.bss
		.align 4
		.type   accum, @object
		.size   accum, 4
accum:
		.zero   4
		.text
		.globl  sum
		.type   sum, @function
sum:
.LFB0:
		.cfi_startproc
		pushq   %rbp
		.cfi_def_cfa_offset 16
		.cfi_offset 6, -16
		movq    %rsp, %rbp
		.cfi_def_cfa_register 6
		movl    %edi, -20(%rbp)
		movl    %esi, -24(%rbp)
		movl    -24(%rbp), %eax
		movl    -20(%rbp), %edx
		addl    %edx, %eax
		movl    %eax, -4(%rbp)
		movl    accum(%rip), %edx
		movl    -4(%rbp), %eax
		addl    %edx, %eax
		movl    %eax, accum(%rip)
		movl    -4(%rbp), %eax
		popq    %rbp
		.cfi_def_cfa 7, 8
		ret
		.cfi_endproc
.LFE0:
		.size   sum, .-sum
		.ident  "GCC: (GNU) 4.8.5 20150623 (Red Hat 4.8.5-28)"
		.section        .note.GNU-stack,"",@progbits
[root@localhost hello]#

3.2.3 查看反汇编代码-汇编指令 (objdump -d code.o)

[root@localhost hello]# gcc -c code.c
[root@localhost hello]# ls -l
total 56
-rw-r--r--. 1 root root    74 Nov  9 15:13 code.c
-rw-r--r--. 1 root root  1408 Nov 10 03:55 code.o
[root@localhost hello]# objdump -d code.o

code.o:     file format elf64-x86-64

Disassembly of section .text:

0000000000000000 <sum>:
	这是汇编指令				这是汇编代码操作
0:   55                      push   %rbp
1:   48 89 e5                mov    %rsp,%rbp
4:   89 7d ec                mov    %edi,-0x14(%rbp)
7:   89 75 e8                mov    %esi,-0x18(%rbp)
a:   8b 45 e8                mov    -0x18(%rbp),%eax
d:   8b 55 ec                mov    -0x14(%rbp),%edx
10:   01 d0                   add    %edx,%eax
12:   89 45 fc                mov    %eax,-0x4(%rbp)
15:   8b 15 00 00 00 00       mov    0x0(%rip),%edx        # 1b <sum+0x1b>
1b:   8b 45 fc                mov    -0x4(%rbp),%eax
1e:   01 d0                   add    %edx,%eax
20:   89 05 00 00 00 00       mov    %eax,0x0(%rip)        # 26 <sum+0x26>
26:   8b 45 fc                mov    -0x4(%rbp),%eax
29:   5d                      pop    %rbp
2a:   c3                      retq   
[root@localhost hello]#

3.2.4 反汇编代码说明(汇编指令特性):

1.GAS:GCC是按照它自己的格式产生汇编代码的,这种格式称为GAS( Gnu assembler,GNU汇编器)
2.上面代码中每个缩进去的行都对应一条机器指令。
3.看到全局变量 accum 的引用,因为此时编译器还不能确定它放到存储器的哪个位置

汇编指令特性:

1.IA32指令长度从1~15个字节不等。
	指令编码被设计成使常用的指令以及操作数较少的指令所需的字节数少,
	而那些不太常用或操作数较多的指令所需字节数较多
	如: 
	1个字节:55                 push   %rbp
	6个字节:8b 15 00 00 00 00  mov    0x0(%rip),%edx # 1b <sum+0x1b>
	
2.指令格式是按照这样一种方式设计的,从某个给定位置开始,可以将字节惟一地解码成机器指令。
	例如,只有指令 pushl%ebp 是以字节值55开头的
	
3.反汇编器只是根据目标文件中的字节序列来确定汇编代码的。
	它不需要访问程序的源代码或汇编代码。
	
4.反汇编器使用的指令命名规则与GAS(GNU汇编器)使用的有些细微的差别。
	在我们的示例中,它省略了很多指令结尾的”l“
	
5.与 code.s 中的汇编代码相比,我们还发现结尾多了一条nop指令。
	这条指令根本不会被执行(它在过程返回指令之后),即使执行了也不会有任何影响
	(所以称之为nop,是“ no operation”的简写,通常读作“nop”)。
	编译器插入这样的指令是为了填充存储该过程的空间

3.2.5 objdump 说明

readelf 和 objdump 例子详解及区别
https://blog.csdn.net/lqy971966/article/details/106905237

3.2.6 code.c + main.c

main.c

int main()
{
		return sum(1, 3);
}

反汇编 prog

[root@localhost hello]# gcc -o prog code.o main.c 
[root@localhost hello]# objdump -d prog 
prog:     file format elf64-x86-64
Disassembly of section .init:
略…………
…………
00000000004004cd <sum>:
4004cd:       55                      push   %rbp
4004ce:       48 89 e5                mov    %rsp,%rbp
4004d1:       89 7d ec                mov    %edi,-0x14(%rbp)
4004d4:       89 75 e8                mov    %esi,-0x18(%rbp)
4004d7:       8b 45 e8                mov    -0x18(%rbp),%eax
4004da:       8b 55 ec                mov    -0x14(%rbp),%edx
4004dd:       01 d0                   add    %edx,%eax
4004df:       89 45 fc                mov    %eax,-0x4(%rbp)
4004e2:       8b 15 40 0b 20 00       mov    0x200b40(%rip),%edx        # 601028 <__TMC_END__>
4004e8:       8b 45 fc                mov    -0x4(%rbp),%eax
4004eb:       01 d0                   add    %edx,%eax
4004ed:       89 05 35 0b 20 00       mov    %eax,0x200b35(%rip)        # 601028 <__TMC_END__>
4004f3:       8b 45 fc                mov    -0x4(%rbp),%eax
4004f6:       5d                      pop    %rbp
4004f7:       c3                      retq   

00000000004004f8 <main>:
……  
[root@localhost hello]#

区别:
这段代码与 code c反汇编产生的代码几乎完全一样。

1.一个主要的区别是左边列出的地址不同(链接器将代码的地址移到一段不同的地址范围)
2.第二个不同之处在于链接器终于确定存储全局变量 accum的地址。
	code.o反汇编代码的第6行中, accum的地址还是0。
	15:   8b 15 00 00 00 00  mov    0x0(%rip),%edx  # 1b <sum+0x1b>
	
	prog的反汇编代码是 0x200b40
	4004e2:       8b 15 40 0b 20 00  mov  0x200b40(%rip),%edx  # 601028
	从最低位到最高位: 40 0b 20 00

3.3 关于格式的注释

其实就是说明:
类似这样

pushl %ebp	Save frame pointer	这就是注释
mov1  %esp,%ebp	Create new frame pointer

4. 数据格式

由于是从16位体系结构扩展成32位的,Inte用术语“字(wod)”表示16位数据类型。
因此称32位数为“双字( double words)”,称64位数为“四字(quad words)”。
我们将遇到的大多数指令都是对字节或双字操作的。

3.1 图
在这里插入图片描述

5. 访问信息

一个IA32中央处理单元(CPU)包含一组八个存储32位值的寄存器,这些寄存器用来存储整数数据和指针。
这八个寄存器,它们的名字都是以%e开头的,不过它们都有特殊的名
字。

5.1 32位下8个常用寄存器:

数据寄存器:eax ebx ecx edx,分别是累加,基址,计数,数据寄存器
变址寄存器:esi sdi,源索引,目的索引寄存器
指针寄存器:ebp ssp,栈底,栈顶寄存器

(%bp和%esp)保存着指向程序栈中重要位置的指针,只有根据栈管理的标准惯例才能修改这两个寄存器中的值。

寄存器(1)寄存器概念,种类说明及汇编代码详解
https://blog.csdn.net/lqy971966/article/details/106780755

5.2 操作数指示符(三种类型)

大多数指令有一个或多个操作数( operand),指示出执行一个操作中要引用的源数据值,以及放置结果的目的位置。

IA32支持多种操作数格式(图3.3)
在这里插入图片描述

各种操作数的可能性被分为三种类型。

第一种是立即数( immediate),也就是常数值。
	立即数的书写方式是“$”后面跟一个整数,比如,$-577或$0xF。
	任何32位的字都可以用做立即数,不过汇编器在可能时会使用一个或两个字节的编码。
	如:sub    $0x20,%rsp 
		movl   $0x0,-0x8(%rbp)
		
第二种类型是寄存器( register),它表示某个寄存器的内容
	对双字操作来说,可以是八个32位寄存器中的一个(如%eax)
	如:push   %rbp
		mov    %eax,%esi
		
第三类操作数是存储器引用,它会根据计算出来的地址(通常称为有效地址)访问某个存储器位置。
	因为将存储器看成一个很大的字节数组
	可以看成内存:
	如:mov1 $-17,		  (%esp) 立即数-内存
		movl %eax,       -12(ebp) 寄存器-内存

5.2 数据传送指令

最频繁使用的指令是执行数据传送的指令。
操作数符号的通用性使得一条简单的传送指令能够完成许多机器中要好几条指令才能完成的功能。
图34列出的是一些重要的数据传送指令,最常用的是传送双字的movl指令。
3.4
在这里插入图片描述

源操作数指定一个值,它可以是立即数,可以存放在寄存器中,也可以存放在存储器中。
目的操作数指定一个位置,它可以是寄存器,也可以是存储器地址。

5.2.1 限制(两个操作数不能都指向存储器位置)

IA32加了一条限制传送指令的两个操作数不能都指向存储器位置。
如:

将一个值从一个存储器位置拷到另一个存储器位置需要两条指令
第一条指令将源值加载到寄存器中
第二条将该寄存器值写入目的位置。

5.2.2 5种可能组合示例

		源操作数      目的
1. movl $0x4050,      %eax	 立即数-寄存器
2. movl %ebp,    	  %esp	 寄存器-寄存器
3. movl (%edi, %ecx), %eax   内存-寄存器
4. mov1 $-17,		  (%esp) 立即数-内存
5. movl %eax,       -12(ebp) 寄存器-内存

5.2.3 movb,movsbl 和 movzbl 的区别

movsbl 和 movzbl 指令负责拷贝一个字节,并设置目的操作数中其余的位。

movb 不改变,只传送一个字节
movsbl 指令的源操作数是单字节的,它执行符号扩展到32位(也就是,将高24位设置为源字节的最高位),
	然后拷贝到双字的目的中。
movzbl 指令的源操作数是单字节的,在前面加24个0扩展到32位,并将结果拷贝到双字的目的中。

例子:
初始假设%df=8D,%eax=98765432

movb %df, %al	 %eax=9876548D	//movb 不改变其他字节
movsbl %df, %eax %eax=FFFFFF8D	//movsbl 将其他三个字节全设置为全1或0
movzbl %df, %eax %eax=0000008D	//movzbl 将其他三个字节全设置为全0

5.2.4 pushl %ebp

pushl %ebp 等价于下面两条指令:

subl $4 $esp		//先将栈指针-4
movl %ebp ($esp)	//然后将值写到新的栈顶地址

5.3 数据传送示例

5.3.1 指针间接引用

int iNum = *ip;
//读取存储在ip地址中的值,然后将它放在名字为iNum的变量中
这个操作成为指针的间接引用

5.3.2 exchange 汇编示例

int exchange(int *xp, int y)
{					//1 movl 8(%ebp), %eax 获取 xp	  |-----|栈底 栈中值存放		
	int x = *xp;	//2 movl 12(%ebp) %edx 获取 y     |    |4
					//3 movl (%eax), %ecx 获取 x      |  xp |8
	*xp = y;		//4 movl %edx, (%eax) 存 y 到 *xp |  y  |12
	return x;		//5 movl  %ecx,%eax 	返回 x    |   x |栈顶
}

汇编详细解释:
当过程体开始执行时,过程参数xp和y存储在相对于寄存器%ebp中地址值的偏移8和12的地方。
指令1和2会将这些参数传送寄存器%eax和%edx。
指令3间接引用xp,并将值存储在寄存器%ecx中,对应于程序值x。
指令4将y存储在xp。
指令5将x传送到寄存器%eax。
根据惯例,所有返回整数或指针值的函数都是通过将结果放在寄存器%eax中来达到目的的,因此这条指令实现了C代码中第6行的功能。
这个例子说明mov指令是如何用于从存储器中读值到寄存器的(指令1~3),如何从寄存器写到存储器的(指令4),以及如何从一个寄存器拷贝到另一个寄存器的(指令5)

5.4 压入和弹出栈数据

push pop

6. 算术和逻辑操作

6.1 加载有效地址(lea指令)

加载有效地址( Load Effective Address)指令lea实际上是mol指令的变形
它的指令形式是从存储器读数据到寄存器,但实际上它根本就没引用存储器。
它的第一个操作数看上去是一个存储器引用,但该指令并不是从指定的位置读入数据,而是将有效地址写入到目的操作数。

图:3.7
在这里插入图片描述

注意,GAS中的操作数顺序与上表相反

加载有效地址(lea)指令通常用来执行简单的算术操作,而其余的指令是非常标准的一元或多元操作。

6.1.1 lea 指令(传地址)

lea load effective address LEA指令的功能是取偏移地址,指令形式是从存储器读数据到寄存器, 效果是将存储器的有效地址写入到目的操作数, 类似, C语言中的”&”.

它的指令形式是从存储器读数据到寄存器,但实际上它根本就没有引用存储器。
他只是取地址但是并没有取地址对应的值。

lea指令只有一个周期,某些编译器会使用lea来优化加法等操作

6.1.2 mov 和 lea 的区别

mov是将数据从源操作传到目的操作数中
lea是将源操作数的地址传到目的操作数中

注:寄存器中存的是地址
一个是数据,一个是地址
LEA指令的功能是取偏移地址,MOV指令的功能是传送数据 
lea传送的是寄存器里面的值,mov传送的是主存中以寄存器的值为地址里面的值。

示例1:

leal 18(%eax),%ebx
movl 18(%eax) ,%ebx
注:寄存器中存的是地址
这样,两条指令,传送的值是不一样的
leal 是传送 18+%eal(值)到 寄存器%ebx
movl 是传送的是在主存中以18+%eax为地址的存储序列里面存的值到%ebx。

示例2:

leal 6(%eax), %edx //把eax的值+6放入edx中。
leal (%eax, %ecx), %edx //把eax+ecx的值装入edx中。
leal (%eax, %ecx, 4), %edx //把eax + 4*ecx的值装入edx中。
leal 7(%eax, %eax, 8), %edx //把9*eax +7的值装入edx中。
leal 0xA(,%eax,4), %edx //把4*eax + 10的值装入edx中。
leal 9(%eax, %ecx, 2), %edx //把eax + 2*ecx+ 9的值装入edx中。

6.2 一元和二元操作

第二类操作是一元操作,只有一个操作数,既作源,也作目的。
这个操作数可以是一个寄存器,也可以是一个存储器位置。
比如说,指令incl(esp)会使栈顶元素加1。

这种语法让人想起C中的加1运算符(++)和减1运算法(--)。

第三类是二元操作,第二个操作数既是源又是目的。
这种语法让人想起C中像+=这样的赋值运算符。
不过,要注意,源操作数是第一个,目的操作数是第二个,这是不可交换操作特有的。

例如,指令sub1 %eax,%edx 使寄存器%edx的值减去%eax中的值 

6.3 移位操作

最后一类是移位操作,先给出移位量,然后是待移位的以进行算术和逻辑右移。
移位量用单个字节编码,因为只允许进行0到31位的移位。移位量可以是立即数,或者放在单字节寄存器元素%cl

图3.7.1
在这里插入图片描述

左移指令有两个名: sall和shll。两者的效果都一样,都是将右边填上0。
右移指令不同,sarl执行算术移位〔填上符号位),而shrl执行逻辑移位(填上0)

6.4 讨论(汇编代码例子)

除了右移操作,其他所有的指令不去区分有符号和无符号数。

例子:

int arith(int x, int y, int z)
{
	int t1 = x + y;
	int t2 = z*48;
	int t3 = t1&0xFFFF;
	int t4 = t2*t3;
	
	return t4;
}

汇编代码:

movl 12(%ebp),%eax 		取y
movl 16(%ebp),%edx		取z
addl 8(%ebp),%eax		t1=x+y
leal (%edx,%edx,2),%edx	3*z
sall $4,%edx			sall左移,z左移4位。1<<4 = 10000 16 
andl $65535 %eax		and 与操作,y&0xffff	
imull %eax,%edx			imul 乘法操作 
movl %edx,%eax			t2*t3

C语言-移位,位域,和运算符优先级
https://blog.csdn.net/lqy971966/article/details/99682879

6.5 特殊的算术操作

imull 双操作数 乘法指令
  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值