Featured image of post 《程序员的自我修养》读书笔记(更新中)

《程序员的自我修养》读书笔记(更新中)

阅读,记录,实践,分享

前言

经学长推荐最近在学习《程序员的自我修养》这本书,是清华大学出版社出版的作品。这本书的内容非常多,所以写篇读书笔记

我们将学到什么?–链接、装载与库

  • WIN和Linux的操作系统下的可执行文件和目标文件格式
  • C/C++程序代码如何被编译成目标文件和在其中的存储
  • 目标文件的链接和执行、符号处理、重定向和地址分配
  • 什么是动态链接,为何动态链接,WIN和Linux如何动态链接和动态链接的相关问题
  • 可执行文件如何被装载并执行,与进程的虚拟空间之间如何映射
  • 堆与栈
  • 函数调用惯例,运行库,Glibc和MSVC CRT的实现分析

基础要求:x86汇编语言基础、C/C++、操作系统基本概念和基本编程技巧、计算机系统结构基本概念

1. 温故而知新

从Hello World说起

一个"Hello World"程序,带领无数人进入了程序的世界,而 简单的背后往往有很多复杂的机制,因此到了某些细节会模糊

1
2
3
4
5
6
#include<stdio.h>

int main(){
    printf("Hello World!\n");
    return 0;
}

带着这些思考进入之后的学习:

  • 程序为什么要被编译器编译了之后才可以运行?
  • 编译器在把C语言程序转换成可以执行的机器码的过程中做了什么,怎么做的?
  • 最后编译出来的可执行文件里面是什么?除了机器码还有什么?它们怎么存放的,怎 么组织的?
  • #include <stdio.h>是什么意思?把stdio.h包含进来意味着什么?C语言库又是什么?它 怎么实现的?
  • 不同的编译器(Microsoft VC、GCC)和不同的硬件平台(x86、SPARC、MIPS、ARM), 以及不同的操作系统(Windows、Linux、UNIX、Solaris),最终编译出来的结果一样 吗?为什么?
  • Hello World 程序是怎么运行起来的?操作系统是怎么装载它的?它从哪儿开始执行, 到哪儿结束? main 函数之前发生了什么?main 函数结束以后又发生了什么?
  • 如果没有操作系统,Hello World 可以运行吗?如果要在一台没有操作系统的机器上运 行 Hello World 需要什么?应该怎么实现?
  • printf是怎么实现的?它为什么可以有不定数量的参数?为什么它能够在终端上输出字 符串?
  • Hello World 程序在运行时,它在内存中是什么样子的?

软件、系统

软件

  • 系统软件:传统意义上指用于管理计算机本身的软件
    • 可分为两类:
      • 平台性:如操作系统内核、驱动程序、运行库、数以千计的系统工具
      • 程序开发:编译器、汇编器、链接器等开发工具和开发库

计算机系统软件体系结构采用一种层的结构,有人说过一句名言:
“计算机科学领域的任何问题都可以通过增加一个间接的中间层来解决”
“Any problem in computer science can be solved by another layer of indirection.”

计算机的整个体系结构从上到下都是按照严格的层次结构设计的,每个层次之间进行通信的协议就是
接口(interface),层次之间遵循这个接口的话任何一个层都能被修改或被替换。 除硬件和应用程序外,其他的都是中间层,而每个中间层都是对下面层的包装和扩展,由此应用程序和硬件保持相对独立(兼容性)
虚拟机则是在硬件和操作系统之间增加了一层虚拟层,由此一个计算机上可以同时运行多个操作系统

  • 应用程序(最上层):网络浏览器、多媒体播放器、文本编辑器、游戏等
    • 应用程序编程接口:开发工具和应用程序使用的接口
    • 接口提供者:运行库——WIN32 API、Glibc

系统

操作系统:管理硬件资源,使得硬件资源能充分发挥作用

  • CPU: 计算机发展早期,CPU 资源十分昂贵,如果一个CPU只能运行一个程序,那么当程序读写磁盘(当时可能是磁带)时,CPU 就空闲下来了,这在当时简直就是暴珍天物 为了协调CPU、内存和高速的图形设备,人们专门设计了一个高速的北桥芯片,以便它们之间能够高速地交换数据。而南桥是连接低频设备的,最初cpu频率甚至和内存差不多
    有了监控系统之后,当某个程序暂时无须使用 CPU时,监控程序就把另外的正在等待 CPU 资源的程序启动,但是最大的问题是程序之间的调度策略太粗糙。对于多道程序来说,程序之间不分轻重缓急,如果有些程序急需使用 CPU 来完成一些任务(比如用户交互的任务),那么很有可能很长时间后才有机会分配到CPU。

经过稍微改进,程序运行模式变成了一种协作的模式,即每个程序运行一段时间以后都主动让出 CPU 给其他程序,使得一段时间内每个程序都有机会运行一小段时间。这对于一 些交互式的任务尤为重要,比如点击一下鼠标或按下一个键盘按键后,程序所要处理的任务 可能并不多,但是它需要尽快地被处理,使得用户能够立即看到效果。这种程序协作模式叫 做分时系统(Time-Sharing System)(但如果一个程序霸占了CPU就会类似while(1),直接死循环。。)

程序员肯定不想天天跟硬件打交道(然而早期程序员又不得不这么做😂),所以操作系统逐步发展,在成熟之后硬件被抽象成一系列概念(NIX 中,硬件设备的访问形式跟访问普通的文件形式一样;在 Windows系统中,图形硬件被抽象成了 GDI,声音和多媒体设备被抽象成了DirectX 对象;磁盘被抽象成了普通文件系统,等等)

  • 文件系统: 文件系统保存了这些文件的存储结构,负责维护这些数据结构并且保证磁盘中的扇区能 够有效地组织和利用。

  • 内存: 一个重要问题:如何将计算机上有限的物理内存分配给多个程序使用

假设我们的计算机有128 MB 内存,程序A运行需要10MB,程序B需要100MB,很容易想到的一种解决方案是:A分配前10MB,B分配10~110MB,但是很明显这样有问题

  1. 内存利用率低,假设有个C程序需要20MB,这里就需要将B先换出到磁盘,然后再分配给C,有大量的数据换入换出了
  2. 地址空间不隔离:直接访问物理地址,恶意程序很容易直接改写进行破坏整个内存空间中的程序(有联想到pwn中未开启ASLR吗)
  3. 程序运行地址不确定,每次程序装入分配的空间位置不确定 因此我们需要的是把程序给出的地址看作是一种虚拟地址,通过映射再进行转化,从而程序物理空间互不重叠。

关于隔离

地址空间分为两种:虚拟地址空间(Virtual Address Space)和物理地址空间(Physical Address Space)
物理地址空间是实实在在存在的,存在于计算机中,而且对于每台计算机只有唯一一个,把物理空间想象成物理内存,比如一台Intel的Pentium4的处理器,它是32位的机器,物理空间有4GB,但是计算机上如果只装了512MB的内存,实际上物理地址的有效部分就是0x00000000 ~ 0x1FFFFFFF
虚拟地址空间则比较抽象,是指虚拟的、人们想象出来的地址空间,而实际上并不存在,每个进程都有自己独立的虚拟空间,而且每个进程只能访问自己的地址空间,由此有效地做到了进程隔离 每个进程有自己的虚拟地址空间 每个进程在操作系统中都会被分配一个独立的虚拟地址空间。这个虚拟空间对于每个进程来说是连续的,且相互之间不会干扰。例如:

  • 进程 A 可能会把自己的虚拟地址空间分配在 0x000000000x7FFFFFFF 之间。
  • 进程 B 则会有一个完全不同的虚拟地址空间,比如 0x000000000x7FFFFFFF,虽然它和进程 A 的虚拟地址空间在地址范围上重叠,但操作系统确保它们彼此隔离,不会互相访问。
    进程只能访问自己的虚拟空间 尽管所有进程的虚拟地址空间可能存在重叠,但操作系统通过内存管理单元(MMU)和页表来确保:
本博客已稳定运行
发表了14篇文章 · 总计3万2千字

浙ICP备2024137952号 『网站统计』

𝓌𝒶𝒾𝓉 𝒻ℴ𝓇 𝒶 𝒹ℯ𝓁𝒾𝓋ℯ𝓇𝒶𝓃𝒸ℯ
使用 Hugo 构建
主题 StackJimmy 设计
⬆️该页面访问量Loading...