操作系统 - Charles-Charmless/Charles-Charmless.github.io GitHub Wiki
操作系统可以看成应用程序和软件之间插入的一层软件。
操作系统的两个基本功能:
- 防止硬件被失控的程序滥用
- 向应用程序提供简单一致的机制来控制复杂而又通常大不相同的低级硬件设备。
主要实现:进程,虚拟内存,和文件。
文件是对IO设备的抽象,
虚拟内存是对主存和磁盘IO设备的抽象
进程是对处理器,主存和IO设备的抽象(对一个正在运行的程序的抽象)。
虚拟机:对整个计算机的抽象,包括操作系统,处理器和程序
操作系统的上下文切换:进程切换表现为同时有很多进程在执行。
上下文:进程运行所需的所有状态信息。
操作系统内核负责切换进程
小端法:最低有效字节在前
大端法:最高有效字节在前
虚拟内存,提供假象:每个进程都在独占的使用主存,每个进程看到的内存都是一致的成为虚拟地址空间
栈区(stack)— 由编译器自动分配释放 ,存放函数的参数值,局部变量的值等。其操作方式类似于数据结构中的栈。
堆区(heap) — 一般由程序员分配释放, 若程序员不释放,程序结束时可能由OS回收 。注意它与数据结构中的堆是两回事,分配方式倒是类似于链表,
全局区(静态区)(static)—,全局变量和静态变量的存储是放在一块的,初始化的全局变量和静态变量在一块区域, 未初始化的全局变量和未初始化的静态变量在相邻的另一块区域。 - 程序结束后有系统释放
4、文字常量区—常量字符串就是放在这里的。 程序结束后由系统释放
5、程序代码区—存放函数体的二进制代码。
stack:
由系统自动分配。 例如,声明在函数中一个局部变量 int b; 系统自动在栈中为b开辟空间
heap:
需要程序员自己申请,并指明大小,在c中malloc函数
申请后系统的响应
栈:只要栈的剩余空间大于所申请空间,系统将为程序提供内存,否则将报异常提示栈溢出。
堆:首先应该知道操作系统有一个记录空闲内存地址的链表,当系统收到程序的申请时,
https://blog.csdn.net/yingms/article/details/53188974
由于局部变量 buffer 的长度是 4 个字节,而在使用 strcpy 函数的时候并没有判断参数 arg1 的长度,因此,当传入的 arg1 的长度大于 4 个字节时,多出的字节将会依次覆盖掉局部变量 temp、调用者的 EBP、返回地址等。
通过精心构造 arg1 的值,就可以将返回地址覆盖为任意想要的值,以便于跳到 shellcode
并发:一个同时具有多个活动的系统
并行:用并发来时一个系统更快
处理器分类:单核处理器,多核处理器,超线程的多核处理器
流水线的方式加快速度
字节(byte)共有8bit
双字
四字
指令集体系架构
- 符号解析
- 重定位
静态链接器以一组客重定位目标文件和命令行参数位输入,生成一个完全链接的,可以加载和运行的可执行目标文件作为输出
目标文件形式:
- 可重定位目标文件(编译器和汇编器生成)
- 可执行目标文件(链接器生成)
- 共享目标文件(编译器和汇编器生成)
典型的ELF可重定位目标文件
Name | Tags | Files |
---|---|---|
data | 已初始化的全局和静态C变量 | |
bss | 未初始化的全局和静态C变量,以及所有被初始化为0的全局或静态变量 | |
symtab | 符号表,存放在程序中定义的和引用的函数和全局变量的信息。 |
局部C变量运行时被保存在栈中,既不出现在.data节中,也不出现在.bss节中
任何带有static属性申明的全局变量或者函数都是模块(文件)私有的,类似的,任何不带static属性申明的全局变量和函数都是公共的,可以被其他模块访问,可以使用static属性来保护自己的变量和函数。
COMMON符号:未初始化的全局变量。
在编译时,编译器向汇编器输出每个全局符号,或者是强的或者是弱的,函数或已初始化的全局变量是强符号,未初始化的全局变量是弱符号,汇编器将这些信息隐含的编码在可重定位目标文件的符号表中,
规则
- 不允许多个同名的强符号
- 如果有一个强符号和多个弱符号同名,选择强符号
- 如果多个弱符号同名,那么从弱符号中任意选择一个。
在链接目标文件和静态库的时候,如果静态库之间存在以来关系,就要对库的顺序进行排列。
重定位的步骤:
- 重定位节和符号定义
- 重定位节中的符号引用
.init节定义了一个_init的小函数,程序的初始化代码会调用他
任何linux程序都调用execve函数来调用加载器,从而将可执行目标文件中的代码和数据从磁盘复制到内存中,然后通过跳转将程序的第一条指令或入口点来运行该程序,(加载)
linux(x86-64运行时的内存映像)
程序的入口点:_start函数地址,定义在系统目标文件ctrl.o中,_start函数调用系统启动函数_libc_starat_main,该函数定义在libc.so中,它初始化执行环境,调用用户层的main函数,处理main函数的返回值,并且在需要的时候将控制返回给内核。
.a文件:静态链接库
.so文件:动态链接库
动态链接:在创建可执行文件时,静态执行一些链接,然后在程序加载时,动态完成链接过程。
可以加载而无需重定位的代码称为位置无关代码。
操作系统用来实现IO,进程和虚拟内存的基本机制
应用程序使用一个叫做陷阱(trap)或者系统调用(system call)的ECF形式,向操作系统请求服务。
在处理器中,状态被编码为不同的位和信号,状态变化称为事件。
当处理器检测到有事件发生时,通过一张叫做异常表的跳转表进一个间接过程调用(异常),到一个专门设计用来处理这类事件的操作系统子程序(异常处理程序)。当异常处理结束后,根据引起异常的事件类型,会发生以下三种情况之一:
- 处理程序将控制返回给当前指令,即事件发生时正在执行的指令
- 处理程序将控制返回给下一条指令(如果没有异常发生将会执行的下一条指令)
- 处理程序终止被中断的程序。
系统启动时,操作系统分配和和初始化一张称为异常表的状态表。
异常区别于过程调用:
异常的类型:
- 中断(interrupt)
- 陷阱(trap)
- 故障(fault)
- 终止(abort)
中断是异步发生的,陷阱,故障和终止时同步发生的,是执行当前指令的结果,把这类指令称为故障指令
陷阱是有意的异常
最重要的用途是在用户程序和内核之间提供一个向过程一样的接口,叫做系统调用
故障是由错误情况引起的,可能被故障处理程序修正,如果修复成功,就将控制返回到引起故障的指令,从而重新执行它,否则处理程序返回内核中的abort例程,abort例程会终止引起故障的应用程序。
终止是不可回复的致命错误造成的错误,从不将控制权返回给应用程序。
每个系统调用都有一个都有一个唯一的整数号,对应于一个到内核中的跳转表的偏移量,(跳转表,不是异常表)
C语言提供了一组包装函数将系统调用包装起来,称为系统级函数。
所有的linux系统调用的参数都是通过通用寄存器而不是栈传递的。给
一个执行中程序的实例
系统中的每一个进程都运行在某个进程的上下文中,上下文是程序正确运行所需的状态组成,这个状态包括存放在内存中的程序的代码和数据,他的栈,通用目的寄存器中的内容,程序计数器,环境变量和打开文件描述符的几何。
进程提供给应用程序的关键抽象:
- 一个独立的逻辑控制流,它提供一个假象:我们的程序独占的使用处理器
- 一个私有的地址空间,好像我们的程序独立的使用内存系统。
并发流:一个逻辑流的执行在时间上与另一个流重叠(生命周期重叠)
并发:多个流并发的执行的一般现象
并行流:两个流并发的运行在不同的处理器核或计算机上。
进程与线程
-
进程是资源分配的最小单位。
-
线程是程序执行的最小单位,也是处理器调度的基本单位,但进程不是,两者均可并发执行。
-
进程有自己的独立地址空间,每启动一个进程,系统就会为它分配地址空间,建立数据表来维护代码段、堆栈段和数据段,这种操作非常昂贵。而线程是共享进程中的数据,使用相同的地址空间,因此,CPU切换一个线程的花费远比进程小很多,同时创建一个线程的开销也比进程小很多。
-
线程之间的通信更方便,同一进程下的线程共享全局变量、静态变量等数据,而进程之间的通信需要以通信的方式(IPC)进行。不过如何处理好同步与互斥是编写多线程程序的难点。但是多进程程序更健壮,多线程程序只要有一个线程死掉,整个进程也跟着死掉了,而一个进程死掉并不会对另外一个进程造成影响,因为进程有自己独立的地址空间。
-
进程切换时,消耗的资源大,效率高。所以涉及到频繁的切换时,使用线程要好于进程。同样如果要求同时进行并且又要共享某些变量的并发操作,只能用线程不能用进程。
-
执行过程:每个独立的进程有一个程序运行的入口、顺序执行序列和程序入口。但是线程不能独立执行,必须依存在应用程序中,由应用程序提供多个线程执行控制。
优缺点:
线程执行开销小,但是不利于资源的管理和保护。线程适合在SMP机器(双CPU系统)上运行。
进程执行开销大,但是能够很好的进行资源管理和保护,可以跨机器迁移。
何时使用多进程,何时使用多线程?
对资源的管理和保护要求高,不限制开销和效率时,使用多进程。
要求效率高,频繁切换时,资源的保护管理要求不是很高时,使用多线程。
- 管道(pipe),流管道(s_pipe),有名管道(FIFO)
- 信号
- 消息队列
- 共享内存
- 信号量
- 套接字(socket)
- 互斥锁
- 读写锁
- 条件变量
- 信号量
- 令牌