文件描述符表 —— 进程的磁盘资源

如果说基于虚拟页表的虚拟内存空间是进程物理内存资源的抽象,那基于文件描述符表的文件系统就是进程磁盘资源的抽象。通过持有文件描述符,进程可以方便地对磁盘上的特定位置(文件)进行读写。
打个不太准确的比方。如果我们把磁盘也看作一个像物理内存一样的字节序列,那磁盘上的文件就像内存中的分页,文件描述符就像 PTE,文件描述符中的 offset 就像 VPN(PPN)。
本文就来解释进程中的文件描述符表的机制。

Read More

Win 10 无法发现局域网电脑的解决办法

最近整了一台 Surface Go,想在主力 PC 间实现远程桌面和文件共享。
本以为都 2019 年了这些事情肯定“只需要点一下”就好了,实际上还是有很多坑。这篇文章就记录了 “Win 10 不能发现/连接局域网电脑” 的解决办法,基本上能解决所有网络上提到的 Win 10 联机问题(前提是在局域网)。

  • 确保在 “控制面板\网络和 Internet\网络和共享中心\高级共享设置” 勾选了 “启用网络发现” 和 “启用文件和打印机共享”,这是实现任何远程互动的前提
  • 如果要使用远程桌面,必须允许远程访问计算机,让 Cortana 找出来“必须允许远程访问计算机
  • 如果要使用文件共享,必须在 “启用或关闭Windows功能” 中启用 SMB,让 Cortana 找出来“启用或关闭Windows功能
  • 让 Cortana 找出“服务”,设置以下服务自动开启:
    • PNRP Machine Name Publication Service
    • TCP/IP NetBIOS Helper
    • Computer Browser(Browser)
    • Function Discovery host Provider(FDPHOST)
    • Function Discovery Resouce Publication(FDResPub)
    • Network Connections(NetMan)
    • Upnp Device Host(UpnpHost)
    • Peer Name Resolution Protocol (PNRPSvc)
    • Peer Networking Grouping (P2PSvc)
    • Peer Networking Identity Manager (P2PIMSvc)
  • 最后为了省心,干脆重启个电脑吧

Read More

基于异常控制流的进程协作

程序是编译出来保存在磁盘上的文件,进程是运行中的程序的实例。操作系统按照程序文件的描述为进程分配了独立的虚拟内存空间,并由 CPU 执行进程的指令。
在理想情况下,CPU 按顺序执行进程的指令。这些有序的指令流叫控制流。虽然控制流在实现分支或者进行调用函数时也会进行跳转,但这种跳转是程序自身有意控制的,属于正常范畴。
不过也有无意发生的跳转:当 CPU 执行完当前指令 I 后,接下来却执行和 I 毫无逻辑关系的 X。而 X 指令,可能是当前进程中的指令,也可能是其它进程中的指令。这种无意的跳转形成的控制流叫异常控制流(Exceptional Controll FLow)。
为什么会出现异常控制流呢?

Read More

虚拟内存系统(三)—— 分配和释放 HEAP 内存

.data 内存段、.text 内存段、共享对象内存段在程序加载和链接时就确认了一个不会变的地址范围,而 stack 内存段范围随着函数栈而自动伸缩,不在开发者的掌握之中。如果开发者想要自由地使用一点内存,就要通过 malloc 向 heap 内存申请一段空间来使用。
但 heap 内存空间也不是无限的,如何高效利用 heap 内存空间也就显得很重要了。
高效利用 heap 内存的第一关键点在于回收不再使用的空间,第二点在于分配时合理利用空闲空间

Read More

虚拟内存系统(一)—— 从物理到虚拟

在编译程序的汇编代码的时候,使用了很多内存位置。可是一台计算器只有一个物理内存,编译器怎么确保编译时使用的内存地址不会和其它程序冲突呢?
实际上,每个程序完全不用担心自己使用的内存地址和其它程序产生冲突,因为它们所使用的内存地址都是专属于自己的虚拟地址,在实际寻址的过程中再由操作系统翻译成真正的物理内存地址。
也就是说,虽然物理内存只有一个,但是被划分为多个彼此互不干扰的虚拟内存空间。
那这是如何实现的呢?虚拟内存空间寻址又是如何转换成物理内存空间寻址的呢?

Read More

通过链接编译复杂的软件(二)—— 静态链接库、动态链接库和打桩

在分离编译和链接技术的帮助下,修改源代码后的重新构建工作不再那么耗时耗力。更重要的是,分离编译和链接这一个过程为我们提供了一套合作协议。
在这套协议下,编译不仅可以是不同时的,甚至也可以是不同地的——我们可以编译修改后的源文件 A 再链接以前编译的模块 B,也可以编译自己电脑上的源文件 A 再链接其他开发者编译给我们的模块 B。这种特性非常适合使用第三库的场景,它使第三方库容易分发和使用(以及一定程度上的保密)。根据第三方库被链接的时机,发展出了静态链接库(编译时链接)和动态链接库(运行时链接)技术。
此外,符号解析这一步骤使得我们能够插手其中做一些干涉(interposition, 打桩),比如把对库函数 A 的引用解析到自己的函数 B ,在 B 里面做一些处理后再调用库函数 A,从而在无法/不需要修改库的情况下方便快速地实现定制功能。
这篇文章将分析静态链接、动态链接库和打桩技术的实现原理。

Read More

通过链接编译复杂的软件(一)—— 主要步骤和要解决的问题

从汇编的角度理解程序中我们了解到,程序主要是执行汇编指令来操作数据。这些汇编指令和大部分数据,除了其自身的外,也都有固定的运行时内存位置。这些值和内存位置,由程序源码通过编译链接而确定下来,并全部被保存在磁盘上的可执行文件中。操作系统把文件中的指令和数据值加载到内存中对应的位置,再把 PC 寄存器设置到 main 函数指令的开始内存位置,从而开始执行程序。
在编译时,源代码(.c文件)首先通过预处理器来处理宏、注释等编程语言语法上的问题,形成 ASCII 中间件文件(.i文件)。之后,编译器把编程语言翻译成 ASCII 码的汇编语言文件(.s)。最后,汇编器把汇编语言文件翻译成二进制的可执行文件。
理论上来说,只需要编译这一个步骤就能确定汇编指令和变量的值和内存位置了,但是在实际工程中运行时内存位置却是在链接时最终确定的。更详细点说,就是每份源码单独编译成一个 ELF 文件(Executable and Linkable Format,意即可被执行又可被链接),其中只包含了该份源码本身的指令和变量的值,然后通过把所有的 ELF 文件链接在一起,为所有的指令和变量生成独特的运行时内存位置。
这样的好处是,当我们修改一个源代码文件后,只需要重新编译一个文件,再链接所有的文件。而且这种方式还附赠了一个便利:链接第三方库编译出来的 ELF 文件就能使用库提供的功能,免去了获取源码和编译源码的烦恼。
但是这些好处不是免费的,有两个明显的和内存位置相关的问题需要解决:

  • 对于在此处引用但是在其它源码中定义的变量和函数,怎么找到他们?
  • 怎么为每个 ELF 文件中的变量和函数生成独特且不彼此覆盖的内存位置?

解决方案分别可以用关键词概括:符号表重定位

Read More

从汇编的角度理解程序(四)—— 复杂数据结构

这篇文章是我《从汇编的角度理解程序》系列的第四篇。
在前面的三篇文章里,第一篇文章介绍了寄存器和按顺序执行的汇编指令,第二篇文章分析了按顺序执行的指令如何通过跳转实现分支和循环,第三篇文章分析了如何借助寄存器和栈内存实现有复杂状态的函数调用。
这一篇文章将会分析如何通过规范的内存布局和访问规则实现复杂的数据结构——数组(array)、结构体(struct)和联合(union)。

Read More

从汇编的角度理解程序(三)—— 函数调用

我们已经知道,程序是按顺序执行的指令流水线(PipeLine)。分支和循环逻辑,可以通过在流水线中往后跳或往前跳实现。
其实,函数调用也不过是在跳转。调用某个函数,就跳转到那个函数的指令流的开始位置,函数执行完成后,再跳转回来。
函数能获取外部写入的数据(输入),能持有自己独特的数据(本地状态),还能向外部写数据(输出)。而理论上程序所拥有的函数数量是无限的,但是寄存器的数量却很少,无限的函数怎么通过有限的寄存器数量来保存它们各自的数据呢?

Read More