深入理解操作系统(8)第三章:程序的机器级表示(4)struct,union+浮点代码(包括:理解指针/gdb/传值和引用/缓冲区溢出/蠕虫和病毒

1. 异质的数据结构

C提供了两种将不同类型的对象结合到一起来创建数据类型的机制:

结构( structure),用关键字 struct 来声明,将多个对象集合到一个单位中;
联合( union),用关键字 union 来声明,允许用几种不同的类型来引用一个对象。

1.1 struct

1. 将多个对象集合到一个单位中
2. 结构的各个组成部分是用名字来引用的
3. 结构的实现类似于数组的实现
   因为结构的所有组成部分都存放在存储器中连续的区域内,而指向结构的指针就是结构第一个字节的地址。
   看下面例子。

例子:

struct Data{
	int i;
	int j;
	int arr[3];
	int *p;
}

偏移量	0	4	8		12		16		20
内容:	i 	j	arr[0]	arr[1]	arr[2]	p

1.2 union

允许以多种类型来引用一个对象。

union 联合提供了一种方式,能够规避C的类型系统,允许以多种类型来引用一个对象。
联合声明的语法与结构的语法一样,只不过语义相差比较大。
它们不是用不同的域来引用不同的存储器块,而是引用的同一存储器块。

union Data{
	double i;
	char c;
	int arr[3];
	int *p;
}

Data 占8字节

1.2.1 union 应用场景

我们事先知道对一个数据结构中的两个不同域的使用是互斥的,那么将这两个域作为联合的一部分,而不是结构的一部分,会减小分配空间的总量

如:例如,假设我们想实现一个二叉树的数据结构,每个叶了节点都有一个 double的数据值,而每个内部节点都有指向两个孩子节点的指针,但是没有数据。

struct NODE{
	struct NODE *left;
	struct NODE *right;
	double data;
}

那么每个节点需要16个字节,每种类型的节点都要浪费一半的字节。相反,如果我们这样来声明个节点

union NODE{
  struct{
	  union NODE *left;
	  union NODE *right;
  }internel;
  double data;
}

每个节点就只需要8个字节

如果n是一个指针,指向 union NODE*类型的节点,

我们用n->data来引用叶子节点的数据,
而用n->internal.left和n>internal.right来引用内部节点的孩子

这样编码,就没有办法来确定一个给定的节点到底是叶子节点,还是内部节点。
通常的方法是引入一个附加的标志域

union NODE{
  int isLeaf;
  struct{
	  struct NODE *left;
	  struct NODE *right;
  }internel;
  double data;
}info;

占12字节内存

1.3 对齐

1.3.1 数据对齐的优点:

1.优化了处理器和存储器系统之间接口的硬件设计。
2.提高存储器系统的性能。

1.3.2 Linux数据对齐策略

Linux沿用的对齐策略是:

2字节数据类型(例如 short)的地址必须是2的倍数,
而较大的数据类型(例如int、int*、foat和double)的地址必须是4的倍数。

注意,这个要求就意味着个short类型对象的地址的最低位必须等于0。类似地,任何int类型的对象或指针的地址的最低两位必须都是0。

例子:

struct Data{
	int i;
	int j;
	char c;
}

数据对齐后,12字节,char后补3个字符数据对齐

2. 在机器级程序中将控制与数据结合起来

2.1 理解指针

2.1.1 理解指针

1. 每个指针都有一个类型。这个类型表明指针指向的对象是哪一类的。
	如: int *p = NULL;
	指针类型:int*
	对象类型: int 
	指针:     p

2. void* 类型代表通用指针。
	如:malloc函数返回一个通用指针,然后它再被强制类型转换成一个有类型的指针

3. 每个指针都有一个值。这个值是某个指定类型的对象的地址。
	如:1. int *p = NULL;
			*p = 1;
		2. 特殊的NULL(0)值表示该指针没有指向任何地方。
		
4. &取地址操作,指针是用&运算符创建的
	如:int p;
		&p = 0x12345 //强制给p赋值地址
		
5. *取指针值操作,*操作符用于指针的间接引用
	如:int *p = NULL;
		*p = 1;

6. 数组与指针是紧密联系的。
	可以引用一个数组的名字(但是不能修改),就好像它是一个指针变量一样。
	数组引用(例如,a[3])与指针运算和间接引用(例如,*(a+3))有一样的效果
	
7. 指针也可以指向函数。
	这提供了一个很强大的存储( storing)和传递代码引用的功能,
	如: 函数指针 void (*f)(int *p) 
		函数指针f表明它指向一个参数是int* 类型,返回值是void类型的函数。
	
	注意:f两遍的括号是必须的,否则就成了 void * f(int *p) 成了一个函数声明了

2.1.2 参数传递(传值和引用)

函数传参的两种方式:传值和引用

传值: 实际的参数值
		值传递的都是副本
		
引用: 指向该值的指针
		传指针也是传值,只不过它的值是指针类型罢了

传值和引用总结:

1. 函数的参数都是原数据的“副本”,因此在函数内无法改变原数据
2. 函数中参数都是传值,传指针本质上也是传值
   如果想要改变入参内容,则需要传该入参的地址(指针和引用都是类似的作用),
   通过解引用修改其指向的内容

函数参数的传值和传指针(引用)
https://blog.csdn.net/lqy971966/article/details/106011497

2.2 使用GDB调试器

命令				效果
1. 开始和停止
quit				Exit GDB	退出gdb
run					Run your program(give command line arguments here) 开始
kill				Stop your program 停止

2. 断点
break sum			Set breakpoint at entry to function sum 设置sum函数断点
break *0x80483c3	Set breakpoint at address ox80483c3	设置地址断点
delete 1			Delete breakpoint 1					删除断点1,先用info b 查看断点标号
delete				Delete all breakpoints				删除所有断点

3. 执行
stepi 				execute one instruction 继续执行,忽略函数调用
nexti				Like stepi, but proceed through function calls 继续执行,进入函数调用
continue			Resume execution 继续执行,到下一个断点停止
finish				Run until current function retums 跳出函数

4. 检查代码
disas 				Disassemble current function 反汇编当前函数
disas sum 			Disassemble function sum	反汇编指定函数sum
disas 0x80483b7		Disassemble function around address Ox80483b7 反汇编指定地址

5. 检查数据
print %eax			Print contents of %eax in decimal 十进制打印
print /x %eax		Print contents of %eax in hex	  十六进制打印
print /t %eax		Print contents of %eax in binary  二进制进制打印

6. 有用的信息
info frame 			打印栈帧
info register		打印寄存器
info args			打印参数
info local			打印局部变量
help

通俗易懂说GDB调试(一)
https://blog.csdn.net/lqy971966/article/details/88963635
通俗易懂说GDB调试(二)
https://blog.csdn.net/lqy971966/article/details/102812016
通俗易懂说GDB调试(三)总结
https://blog.csdn.net/lqy971966/article/details/103118094

2.3 内存越界引用和缓冲区溢出

2.3.1 缓冲区溢出

1.一种特别常见的状态破坏称为缓冲区溢出( buffer overflow)。
	通常,在栈中分配某个字节数组来保存一个字符串,但是字符串的长度超出了为数组分配的空间。

2. 缓冲区溢出的一个更加致命的使用就是让程序执行它本来不愿意执行的函数。
	这是一种最常见的通过计算机网络攻击系统安全的方法。

缓冲区和缓存
https://blog.csdn.net/lqy971966/article/details/104497600

2.3.2 蠕虫和病毒

蠕虫和病毒都是试图在计算机中传播它们自己的代吗

蠕虫(worm)是这样一种程序,它可以自己运行,并且能够将一个完全有效的自己传播到其他机器。
病毒(virus)是这样一段代码,它能将自己添加到包括操作系统在内的其他程序中,但它不能独立运行。

3. 浮点代码

3.1 浮点的协处理器历史

处理浮点值的指令集是IA32体系结构最不优美的特性之一。

在最早的 Intel机器中,浮点是由个独立的协处理器来完成的,
这个部件有它自己的寄存器和处理能力,能够执行一部分指令。

这个协处理器是由名为8087、80287和i387的独立芯片实现的,且伴随着处理器芯片8086、80286和i386。

在这些产品的开发过程中,芯片的容量已经不足以在一块芯片上既包括主处理器又包括浮点协处理器的。另外,廉价的机器会省去浮点硬件,只用软件来完成浮点操作(非常慢!)。从i486开始,浮点就作为IA32CPU芯片的一部分了1980年,最早的8087协处理器的问世赢得了很高的赞誉。它是第一个单芯片浮点单元(FPU)同时也是IEEE浮点的第一个实现。作为协处理器运行时,在主处理器取出浮点指令后,FPU会接过它们完成执行。FPU和主处理器之间有最少限度的连接。将数据从一个处理器传递到另一个,需要发送方处理器写存储器,接收方处理器再从存储器中读取。直到今天IA32浮点指令集中还保留有这些设计的遗迹。另外,1980年的编译技术比今天的简陋得多。对于优化编译器来说,IA32浮点的许多特性都是很难的目标。

3.2 浮点寄存器

浮点单元包括8个浮点寄存器,但是和普通寄存器不一样,这些寄存器是被当成一个浅栈( shallow stack)来对待的。
这些寄存器分别标识为%st(0)、%st(1),等等,直到%st(7)。
其中,%st(0)在栈顶。当压入栈中的值超过8个时,栈底的那些值就会消失。

3.3 栈的表达式求值

理解AI32是如何使用浮点寄存器做栈的

3.4 浮点数据的传送和转换

用记符%st(i)来引用浮点寄存器,这里i代表相对于栈顶的位置。值i的范围为0~7。寄存器%st(0)是栈顶元素,%st(1)是第二个,依此类推。也可以用%st来引用栈项元素。当一个新值压入栈中时,寄存器%st(7)中的值就丢失了。当从栈中弹出时,%(7)中的新值是不可预测的。编译器产生的代码必须能在寄存器栈有限的容量中工作。

3.5 浮点算术指令

图3.33
在这里插入图片描述

上图说明了一些最常见的浮点算术操作。
第一组中的指令没有操作数。它们将某些常数数字的浮点表示压入栈中。对像π、e和log210这样的常数,也有类似的指令。
第二组中的指令有一个操作数。这个操作数总是栈顶的元素,类似于假设的栈求值器中的neg操作,它们会用计算出的值取代这个元素。
第三组中的指令有两个操作数。对每个这样的指令,都有关于如何指定操作数的许多不同的变种,待会儿会谈到。对不可交换操作,例如减法和除法,有前向(例如fsub)和反向(例如 fsubr)两个版本,这样就可以按照两种顺序中的任一种来使用参数。

3.6 在过程中使用浮点

同整数参数一样,浮点参数是通过栈传递给调用过程的。
每个foat类型的参数需要4个字节的栈空间,而每个 double类型的参数需要8个字节。对于返回值为foat或 double 类型的函数,结果是以扩展精度格式在浮点寄存器栈顶部返回的。

作为一个示例,看看下面这个函数:

double func(double a, float x, double b, float y)
{
	return a*x - b/y;
}

3.7 浮点值的比较

类似于整数的情况,确定两个浮点数的相对值包括用比较指令来设置条件码,然后再测试这些条件码。
不过,对于浮点,条件码是浮点状态字的一部分,浮点状态字是一个16位寄存器,包含关于浮点单元的各种标志。必须将这个状态字转换成整数字,然后测试某些特殊的位。

3.8 在c中嵌入汇编代码

在早期的计算中,大多数程序都是用汇编代码写的,即使是很大型的操作系统也是在没有高级语占帮助的情况下编写的。就程序的复杂性来说,这就变得难以管理了。因为汇编代码不提供任何形式的类型检查,所以很容易犯基本的错误,例如将指针作为整数来用,而不是间接引用指针。更糟的是,用汇编写代码会将整个程序限制在某一类机器上了。重写一个汇编语言程序,使它能在不同的机器上运行,与从头写整个程序是一样困难的。

不过现在,优化编译器基本上使得性能优化不再是用汇编代码写程序的一个原因了。

3.9 基本的内嵌汇编

GCC还可以将汇编与C代码混合起来。内嵌汇编允许用户直接往编译器产生的代码序列中插入汇编代码。可以提供一些特性,以指定指令操作数和向编译器说明汇编指令要覆盖哪些寄存器。当然,得到的代码是与机器高度相关的,因为不同类型机器的机器指令是不兼容的。

4. 第三章总结:

在本章中,我们窥视了高级语言提供的抽象层下面的东西,以了解机器级编程。

1. 通过让编译器产生机器级程序的汇编代码表示,我们了解了编译器和它的优化能力,
	以及机器代码、它的数据类型和它的指令集。

2. 我们还看了一些高级语言抽象隐藏有关程序操作重要细节的例子。
	例如,浮点代码的行为可能依赖于值是保存在寄存器中,还是在存储器中。
	
3. 汇编语言与C代码差别很大。
	a. 在汇编语言程序中,各种数据类型之间的差别很小。
	b. 程序是以指令序列来表示的,每条指令都完成一个单独的操作。
	c. 部分程序状态,如寄存器和运行时栈,对程序员来说是直接可见的。
	   仅提供了低级操作来支持数据处理和程序控制。
	d. 编译器必须用多条指令来产生和操作各种数据结构,来实现像条件、循环和过程这样的控制结构。
	e. 我们看到C中缺乏边界检査,使得许多程序容易出现缓冲区溢出,
		而这已经使许多系统容易受到入侵者的恶意攻击。
		
4. 我们只分析了C到IA32的映射,但是我们讲的大多数内容对其他语言和机器组合来说也是类似的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值