《深入理解计算机系统》笔记(一)栈(本篇)
欢迎查看《深入理解计算机系统》系列博客
《深入理解计算机系统》笔记(一)栈(本篇)
《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收
《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】
——————————————————————————————————————–
读后感
这本书是美国“卡内基-梅隆大学(CMU)”的教科书,逻辑严谨。虽然是教科书,还是有些晦涩难懂啊,不太形象。第二章主要讲整数,浮点数,很是晦涩,全是数学公式。作者的思维数学的思维,动不动就是n、m、k、∑等等,让我们数学很烂的同学如何是好。如果能以普通人的思维把数学知识加进去就好了。
该书确实系统的介绍了计算机,很完善。它能给你以下几个重要级别的模型和过程:
1.函数的调用栈模型——第三章(函数不一定都会创建栈帧,本文章将解释此现象)
2.a.out或者exe可执行文件的结构——第七章点击打开链接
3.程序加载器和链接——第八章 点击打开链接
4.malloc和虚拟存储器原理——第九章点击打开链接
5.线程,在存储器中模型——第12章
对于处于成长期的程序员来说,真是欣喜若狂!有了这些知识还需要《C专家编程》这本书么?这本书就是《C专家编程》的全覆盖啊,哈哈!
翻译者很是用心,但是读者不一定领情。比如:可以直接翻译流行的内存、硬盘和固态硬盘,完全没有必要用主存、磁盘和固态存储磁盘。还比如:没有必要把shell翻译成“外壳”多别扭啊。这些翻译者应该像“侯捷”学习。
这本说内容大而散,感觉没有尽头一样。老外怎么学这种课程,费脑子啊。从计算机结构、二进制表示、到汇编语言函数的调用、然后cpu的结构、再有连接器存储器、还有进程,并发、更有网络编程,基本大学四年也就学了这么多东西。
这本书中有句话很有意思:存储器的一个有趣的属性是不论系统中有多大的存储器,他总是一种稀缺资源。磁盘空间和垃圾桶同样有这个属性。
工作2年多的时间里,每每都是在网上搜系统方面的知识、编译、链接和虚拟存储器malloc等等。只有读了这本书才能系统得学到计算机知识。
一、计算机漫游
—》利用直接存储器(DMA)的技术,数据可以不通过cpu而直接从磁盘到达内存。
—》根据机械原理,较大的存储器比较小的存储器运行慢,一个寄存器只能存储几百个Byte,而且内存可以存放GB以上。加快处理器的运行速度比加快内存运行速度更容易。
—》高速缓存至关重要,一个简单的helloworld揭示了一个重要的问题。系统花费大量时间把信息从一个地方挪到另一个地方。helloworld最初放在硬盘上,然后加载到内存,进而进入cpu中。下图说明了一个存储器层次结构:
二、信息的表示和处理
讲的是计算机原理,二进制,补码和浮点数等。因为大学课程已经学习过了,没有细读。
—》浮点数,规格化、非规格化和无穷大。
一般来说我们没把发用小数表示1/3、7/10等这些不能整出的数字,那么如果用二进制表示十进制的小数,更多的表示不出来。二进制甚至不能表示十进制的0.1和0.2
三、程序的机器级表示(其实就是汇编语言)
—》讲的是《汇编语言》,头都大了!个人觉得汇编语言不用花时间了解,即使是本书中的汇编语言也有文字解析。IA32和X86-64两种汇编语言。
—》汇编代码不区分有符号和无符号甚至指针类型。
—》下图展示了,汇编代码后缀的含义:
大多数GCC生成的汇编指令都有一个字符后缀,表示操作数的大小。例如数据传送指令有三个变种:movb(传送字节)、movw(传送字)和movl(传送双字)。注意,汇编代码使用后缀’l’来表示4个字节整数和8个字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。
操作数指示符,操作数一共有三类:1)立即数(immediate)也就是常数值,立即数的书写方式是
—》一个32位cpu中寄存器的结构如下:
上图是IA32的整数寄存器。所有8个寄存器都可以作为32位和16位使用,例如%eax和%ax。并且前四个寄存器可以访问其两个低字节。如:%ah和%al。
下图是64位cpu的寄存器结构图:
红色框内,是兼容32为cpu的结果。
—》寄存器使用惯例:%eax、%edx和%ecx是调用者保存寄存器,%ebx、%esi和%edi是被调用者保存寄存器。那么,一个函数f()可能被别人调用,也可以调用其他函数,所以当f()运行时需要将%ebx、%esi和%edi保存到栈中,并在返回前再恢复它们。(p151)—》64位%rax寄存器用来保存函数的返回值,(p198)
在x86-64汇编语言,中%rax用来保存函数的返回值,而在结果返回之前,%rax可以重复利用。
—》栈在处理函数调用中起到至关重要的作用。下图栈的示意图,栈顶朝下,由于IA32 的栈竟然是往低地址延伸生长,直让我崩溃。(p115)
图片的上半部分,说明了实际效果,即将%eax的值移动到%edx中,图片的下半部分是栈移动步骤。栈顶的变化最后关键。从0x108 -> 0x104 -> 0x108
—》栈帧结构,IA32程序用程序栈来支持函数调用。机器用栈来传递函数参数、返回值、保存寄存器用于以后恢复和本地存储。为单个过程分配的那部分栈成为栈帧(stack frame)。下图说了栈帧的结构。
—》call指令。call指令的效果是将返回值地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址。这样当被调用函数返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:
- int accum = 0;
- int sum(int x,int y);
- int main()
- {
- return sum(1.3);
- }
- int sum(int x,int y)
- {
- int t = x + y;
- accum += t;
- return t;
- }
int accum = 0;
int sum(int x,int y);
int main()
{
return sum(1.3);
}
int sum(int x,int y)
{
int t = x + y;
accum += t;
return t;
}
经过反汇编后,节选处call部分的代码如下图所示:
第一行call指令的效果就是将0x80483e1压入栈中,同时将%eip(程序计数器)的值设置为sum的第一条指令0x8048394.最后一行的ret指令弹出0x80483e1给%eip,并跳转到这个地址。如图所示:
ret指令的效果就是让0x080483e1弹出,调整栈指针,并且0x080483e1赋给%eip,程序继续执行。
—》函数调用实例
- int swap_add(int* xp,int* yp);
- int caller()
- {
- int arg1 = 534;
- int arg2 = 1057;
- int sum = swap_add(&arg1 , &arg2);
- int diff = arg1 - arg2;
- retur sum * diff;
- }
- int swap_add(int* xp,int* yp)
- {
- int x = * xp;
- int y = * yp;
- *xp = y;
- *yp = x;
- return x + y;
- }
int swap_add(int* xp,int* yp);
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1 , &arg2);
int diff = arg1 - arg2;
retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
int x = * xp;
int y = * yp;
*xp = y;
*yp = x;
return x + y;
}
(蓝色箭头是“指向”,红色箭头是“偏移量”,绿色箭头是解释说明)
arg1和arg2必须存放在栈中,因为我们必须为它们生成地址。swap_add中的变量int x和int y可以存放在寄存器中。
分配在栈上的24个字节,8个用于局部变量,8个用于参数,8个未使用,这是因为GCC认识所有的栈空间都应该是16的整数倍。这样保证数据放的严格对齐。
经过调用swap_add之后栈的信息又恢复到最初的状态。
—》许多函数编译后不需要栈帧。如果所有的局部变量都能保存在寄存器中,而且这个函数又不会调用其他函数(叶子过程),那么需要栈的唯一原因就是用来保存返回值。特别是dui’yu所以,虽然C语言中有寄存器变量,但是如果这个函数的变量很少的话,及时不标明这个变量是寄存器,它也会被加载到寄存器中去。(p196)
—》函数需要栈帧的原因有如下几个:
●局部变量太多,不能都放在寄存器中。
●有些局部变量是数组或者结构。
●函数用&来计算一个局部变量的地址。
●函数必须将栈上的某些参数传递给另外一个函数
●在修改一个被调用着保存寄存器之前,函数需要保存其他状态。
—》栈破坏检测和栈保护(p181)
在C语言中,没有可靠的方法来防止对数组的越界写操作。数组越界,是栈溢出后发现这个错误然后抛出。
echo是一个函数,存放了char buf[8]的一个局部变量。
思想:在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,也成为哨兵值(guard value)是在程序每次运行时随机产生的。因此如果这个哨兵值改变了说明栈溢出了。
—》栈随机化(p180)
计算机
比如,多次运行下面的代码,本地变量的地址是不变的。
- int main()
- int local;
- printf(”local at %p\n”,&local);
- return 0;
int main()
{
int local;
printf("local at %p\n",&local);
return 0;
}
一个现实生活中的例子,但是这个例子说的是每次堆上开辟空间可能是一致的。
曾经在做Symbian项目的时候,发现一个不是必现的bug,后来发现是野指针。但是问题是为什么不是必现呢?是因为Symbian操作系统每次在堆上开辟的空间,在短时间内是一个地址。举例:假如,ptr这个指针,现在成为野指针了。但是,之后它指向的内存又被重新malloc了,等同于ptr指向了新的对象。但是,这个巧合并不是每次复现。
—》将IA32扩展到64位。(p183)
X86-64是AMD提出来,并命名的。现在一般简写X64
●通用目的寄存器组从8个扩展到16个。而且名字也变成了%rax,%rbx。其中%rax用来存放返回值。
●许多程序状态都保存在寄存器中,而不是栈上。整形和指针类型的参数通过寄存器传递。所以,有些过程根本不需要建立栈。
●如果可能,条件操作作用条件传送指令实现,会得到比传统分支代码更好的性能。
●浮点操作用面向寄存器的指令集来实现,而不是IA32支持的基于栈的方法来实现。
●X86-64没有帧寄存器。
—》函数指针的值是该函数机器代码表示中的第一条指令的地址。(p173)第二个读书笔记,点击查看
</div>
欢迎查看《深入理解计算机系统》系列博客
《深入理解计算机系统》笔记(一)栈(本篇)
《深入理解计算机系统》笔记(四)虚拟存储器,malloc,垃圾回收
《深入理解计算机系统》笔记(五)并发、多进程和多线程【Final】
——————————————————————————————————————–
读后感
这本书是美国“卡内基-梅隆大学(CMU)”的教科书,逻辑严谨。虽然是教科书,还是有些晦涩难懂啊,不太形象。第二章主要讲整数,浮点数,很是晦涩,全是数学公式。作者的思维数学的思维,动不动就是n、m、k、∑等等,让我们数学很烂的同学如何是好。如果能以普通人的思维把数学知识加进去就好了。
该书确实系统的介绍了计算机,很完善。它能给你以下几个重要级别的模型和过程:
1.函数的调用栈模型——第三章(函数不一定都会创建栈帧,本文章将解释此现象)
2.a.out或者exe可执行文件的结构——第七章点击打开链接
3.程序加载器和链接——第八章 点击打开链接
4.malloc和虚拟存储器原理——第九章点击打开链接
5.线程,在存储器中模型——第12章
对于处于成长期的程序员来说,真是欣喜若狂!有了这些知识还需要《C专家编程》这本书么?这本书就是《C专家编程》的全覆盖啊,哈哈!
翻译者很是用心,但是读者不一定领情。比如:可以直接翻译流行的内存、硬盘和固态硬盘,完全没有必要用主存、磁盘和固态存储磁盘。还比如:没有必要把shell翻译成“外壳”多别扭啊。这些翻译者应该像“侯捷”学习。
这本说内容大而散,感觉没有尽头一样。老外怎么学这种课程,费脑子啊。从计算机结构、二进制表示、到汇编语言函数的调用、然后cpu的结构、再有连接器存储器、还有进程,并发、更有网络编程,基本大学四年也就学了这么多东西。
这本书中有句话很有意思:存储器的一个有趣的属性是不论系统中有多大的存储器,他总是一种稀缺资源。磁盘空间和垃圾桶同样有这个属性。
工作2年多的时间里,每每都是在网上搜系统方面的知识、编译、链接和虚拟存储器malloc等等。只有读了这本书才能系统得学到计算机知识。
一、计算机漫游
—》利用直接存储器(DMA)的技术,数据可以不通过cpu而直接从磁盘到达内存。
—》根据机械原理,较大的存储器比较小的存储器运行慢,一个寄存器只能存储几百个Byte,而且内存可以存放GB以上。加快处理器的运行速度比加快内存运行速度更容易。
—》高速缓存至关重要,一个简单的helloworld揭示了一个重要的问题。系统花费大量时间把信息从一个地方挪到另一个地方。helloworld最初放在硬盘上,然后加载到内存,进而进入cpu中。下图说明了一个存储器层次结构:
二、信息的表示和处理
讲的是计算机原理,二进制,补码和浮点数等。因为大学课程已经学习过了,没有细读。
—》浮点数,规格化、非规格化和无穷大。
一般来说我们没把发用小数表示1/3、7/10等这些不能整出的数字,那么如果用二进制表示十进制的小数,更多的表示不出来。二进制甚至不能表示十进制的0.1和0.2
三、程序的机器级表示(其实就是汇编语言)
—》讲的是《汇编语言》,头都大了!个人觉得汇编语言不用花时间了解,即使是本书中的汇编语言也有文字解析。IA32和X86-64两种汇编语言。
—》汇编代码不区分有符号和无符号甚至指针类型。
—》下图展示了,汇编代码后缀的含义:
大多数GCC生成的汇编指令都有一个字符后缀,表示操作数的大小。例如数据传送指令有三个变种:movb(传送字节)、movw(传送字)和movl(传送双字)。注意,汇编代码使用后缀’l’来表示4个字节整数和8个字节双精度浮点数,这不会产生歧义,因为浮点数使用的是一组完全不同的指令和寄存器。
操作数指示符,操作数一共有三类:1)立即数(immediate)也就是常数值,立即数的书写方式是
—》一个32位cpu中寄存器的结构如下:
上图是IA32的整数寄存器。所有8个寄存器都可以作为32位和16位使用,例如%eax和%ax。并且前四个寄存器可以访问其两个低字节。如:%ah和%al。
下图是64位cpu的寄存器结构图:
红色框内,是兼容32为cpu的结果。
—》寄存器使用惯例:%eax、%edx和%ecx是调用者保存寄存器,%ebx、%esi和%edi是被调用者保存寄存器。那么,一个函数f()可能被别人调用,也可以调用其他函数,所以当f()运行时需要将%ebx、%esi和%edi保存到栈中,并在返回前再恢复它们。(p151)—》64位%rax寄存器用来保存函数的返回值,(p198)
在x86-64汇编语言,中%rax用来保存函数的返回值,而在结果返回之前,%rax可以重复利用。
—》栈在处理函数调用中起到至关重要的作用。下图栈的示意图,栈顶朝下,由于IA32 的栈竟然是往低地址延伸生长,直让我崩溃。(p115)
图片的上半部分,说明了实际效果,即将%eax的值移动到%edx中,图片的下半部分是栈移动步骤。栈顶的变化最后关键。从0x108 -> 0x104 -> 0x108
—》栈帧结构,IA32程序用程序栈来支持函数调用。机器用栈来传递函数参数、返回值、保存寄存器用于以后恢复和本地存储。为单个过程分配的那部分栈成为栈帧(stack frame)。下图说了栈帧的结构。
—》call指令。call指令的效果是将返回值地址入栈,并跳转到被调用过程的起始处。返回地址是在程序中紧跟在call后面的那条指令的地址。这样当被调用函数返回时,执行会从此处继续。ret指令从栈中弹出地址,并跳转到这个位置。例如下面的代码:
- int accum = 0;
- int sum(int x,int y);
- int main()
- {
- return sum(1.3);
- }
- int sum(int x,int y)
- {
- int t = x + y;
- accum += t;
- return t;
- }
int accum = 0;
int sum(int x,int y);
int main()
{
return sum(1.3);
}
int sum(int x,int y)
{
int t = x + y;
accum += t;
return t;
}
经过反汇编后,节选处call部分的代码如下图所示:
第一行call指令的效果就是将0x80483e1压入栈中,同时将%eip(程序计数器)的值设置为sum的第一条指令0x8048394.最后一行的ret指令弹出0x80483e1给%eip,并跳转到这个地址。如图所示:
ret指令的效果就是让0x080483e1弹出,调整栈指针,并且0x080483e1赋给%eip,程序继续执行。
—》函数调用实例
- int swap_add(int* xp,int* yp);
- int caller()
- {
- int arg1 = 534;
- int arg2 = 1057;
- int sum = swap_add(&arg1 , &arg2);
- int diff = arg1 - arg2;
- retur sum * diff;
- }
- int swap_add(int* xp,int* yp)
- {
- int x = * xp;
- int y = * yp;
- *xp = y;
- *yp = x;
- return x + y;
- }
int swap_add(int* xp,int* yp);
int caller()
{
int arg1 = 534;
int arg2 = 1057;
int sum = swap_add(&arg1 , &arg2);
int diff = arg1 - arg2;
retur sum * diff;
}
int swap_add(int* xp,int* yp)
{
int x = * xp;
int y = * yp;
*xp = y;
*yp = x;
return x + y;
}
(蓝色箭头是“指向”,红色箭头是“偏移量”,绿色箭头是解释说明)
arg1和arg2必须存放在栈中,因为我们必须为它们生成地址。swap_add中的变量int x和int y可以存放在寄存器中。
分配在栈上的24个字节,8个用于局部变量,8个用于参数,8个未使用,这是因为GCC认识所有的栈空间都应该是16的整数倍。这样保证数据放的严格对齐。
经过调用swap_add之后栈的信息又恢复到最初的状态。
—》许多函数编译后不需要栈帧。如果所有的局部变量都能保存在寄存器中,而且这个函数又不会调用其他函数(叶子过程),那么需要栈的唯一原因就是用来保存返回值。特别是dui’yu所以,虽然C语言中有寄存器变量,但是如果这个函数的变量很少的话,及时不标明这个变量是寄存器,它也会被加载到寄存器中去。(p196)
—》函数需要栈帧的原因有如下几个:
●局部变量太多,不能都放在寄存器中。
●有些局部变量是数组或者结构。
●函数用&来计算一个局部变量的地址。
●函数必须将栈上的某些参数传递给另外一个函数
●在修改一个被调用着保存寄存器之前,函数需要保存其他状态。
—》栈破坏检测和栈保护(p181)
在C语言中,没有可靠的方法来防止对数组的越界写操作。数组越界,是栈溢出后发现这个错误然后抛出。
echo是一个函数,存放了char buf[8]的一个局部变量。
思想:在栈帧中任何局部缓冲区与栈状态之间存储一个特殊的金丝雀(canary)值,也成为哨兵值(guard value)是在程序每次运行时随机产生的。因此如果这个哨兵值改变了说明栈溢出了。
—》栈随机化(p180)
计算机
比如,多次运行下面的代码,本地变量的地址是不变的。
- int main()
- int local;
- printf(”local at %p\n”,&local);
- return 0;
int main()
{
int local;
printf("local at %p\n",&local);
return 0;
}
一个现实生活中的例子,但是这个例子说的是每次堆上开辟空间可能是一致的。
曾经在做Symbian项目的时候,发现一个不是必现的bug,后来发现是野指针。但是问题是为什么不是必现呢?是因为Symbian操作系统每次在堆上开辟的空间,在短时间内是一个地址。举例:假如,ptr这个指针,现在成为野指针了。但是,之后它指向的内存又被重新malloc了,等同于ptr指向了新的对象。但是,这个巧合并不是每次复现。
—》将IA32扩展到64位。(p183)
X86-64是AMD提出来,并命名的。现在一般简写X64
●通用目的寄存器组从8个扩展到16个。而且名字也变成了%rax,%rbx。其中%rax用来存放返回值。
●许多程序状态都保存在寄存器中,而不是栈上。整形和指针类型的参数通过寄存器传递。所以,有些过程根本不需要建立栈。
●如果可能,条件操作作用条件传送指令实现,会得到比传统分支代码更好的性能。
●浮点操作用面向寄存器的指令集来实现,而不是IA32支持的基于栈的方法来实现。
●X86-64没有帧寄存器。
—》函数指针的值是该函数机器代码表示中的第一条指令的地址。(p173)第二个读书笔记,点击查看
</div>