从一个ELF程序的加载窥探操作系统内核-(4)

从一个ELF程序的加载窥探操作系统内核-(4)

操作系统加载一个ELF程序看似一个EASY的动作,其实下面隐藏了很多很多OS内核的关键实现,让我们一起来解密其中的流程

作者是一个micro kernel的开发者,在设计动态链接器的时候,在此留下一些笔记,重点参考了以下资料文献

  • 《程序员的自我修养》
  • 《深入理解计算机系统》
  • 《现代操作系统-原理与实现》
  • 《深入理解LINUX内核》
  • 《设计模式/JAVA》
为什么需要栈

栈一种先入后出的数据结构,恰好满足我们的设计需求而已,像矩阵存在的意义一样

  • 为什么在使用C语言前要先使用栈
    • 因为C语言里用到了函数,函数的参数传递和返回值,以及函数里的局部变量,都需要通过栈来保存,你不设置好栈,这些内容没地方保存
    • 如果你说我可以使用汇编搞定一切,就当我什么也没说
  • 在任务切换的时候需要栈
    • 在任务切换时,我们找个地方保存当前任务的上下文,不然将来恢复的时候怎么去恢复呢,所以不同任务的栈是不一样的
  • 在中断时需要用栈
    • 进入中断handler中,如果你需要进入c语言的函数处理,也是需要栈的,这其实和C语言需要用栈的原因相同
中断栈/内核栈/进程栈/线程栈

相信从事嵌入式开发的同学对这几种栈都有印象吧?

中断栈

中断栈无论是在MCU还是CPU中都是必须的,当进入中断时,需要进行中断处理,这里大多都用了C语言来做,所以必须要有中断栈,一个CPU下只需要一个中断栈,这个栈的大小在汇编里进行固定分配

进程陷入内核态的时候,需要内核栈来支持内核函数调用。中断也是如此,当系统收到中断事件后,进行中断处理的时候,也需要中断栈来支持函数调用。
由于系统中断的时候,系统当然是处于内核态的,所以中断栈是可以和内核栈共享的。但是具体是否共享,这和具体处理架构密切相关。X86 上中断栈就是独立于内核栈的;独立的中断栈所在内存空间的分配发生在arch/x86/kernel/irq_32.c的irq_ctx_init()函数中(如果是多处理器系统,那么每个处理器都会有一个独立的中断栈),函数使用__alloc_pages在低端内存区分配2个物理页面,也就是8KB大小的空间。
有趣的是,这个函数还会为softirq分配一个同样大小的独立堆栈。如此说来,softirq将不会在hardirq的中断栈上执行,而是在自己的上下文中执行。
在这里插入图片描述

而 ARM 上中断栈和内核栈则是共享的;中断栈和内核栈共享有一个负面因素,如果中断发生嵌套,可能会造成栈溢出,从而可能会破坏到内核栈的一些重要数据,所以栈空间有时候难免会捉襟见肘。

内核栈
  • 内核线程需要有内核栈,典型的0号idle进程,由于他处于内核空间,他需要内核栈
  • 在每一个进程的生命周期中,必然会通过到系统调用陷入内核。在执行系统调用陷入内核之后,这些内核代码所使用的栈并不是原先进程用户空间中的栈,而是一个单独内核空间的栈,这个称作进程内核栈

注意这里中断栈和进程内核栈的区别,中断栈只有一个,进程内核栈为每个进程都有,而且内核栈是发生在系统调用时使用的,其实系统调用本身也是一个中断,一般为SVC中断异常,这个要和普通的IRQ/FIQ中断分开,IRQ/FIQ主要用做设备中断和调度器使用,SVC作为系统调用使用

进程栈

进程栈是属于用户态栈,和进程虚拟地址空间 (Virtual Address Space) 密切相关。那我们先了解下什么是虚拟地址空间:在 32 位机器下,虚拟地址空间大小为 4G。这些虚拟地址通过页表 (Page Table) 映射到物理内存,页表由操作系统维护,并被处理器的内存管理单元 (MMU) 硬件引用。每个进程都拥有一套属于它自己的页表,因此对于每个进程而言都好像独享了整个虚拟地址空间。

进程栈的创建是在执行一个ELF程序的时候创建的,在fork下进程栈就是和父进程共享的,在exec中进程栈是通过COW创建的

每个进程有了单独的进程栈后,及时再发生堆栈溢出,也不会影响其他进程了

线程栈

对于线程来说,他的栈其实是不是由内核来管理的,Linux下通过glibc下的tcmalloc来管理线程的栈,也是就malloc来管理
同一个进程下的多个线程,都是在同一个tcmalloc下来管理的,他们共享地址空间,所以从本质上来说,同一进程下的线程之间的栈是可以互相访问的,这也是造成多线程安全的一个源头,如果一个线程的出现问题,有可能其他线程的栈也被污染

Linux 为什么需要区分这些栈?

为什么需要区分这些栈,其实都是设计上的问题。这里就我看到过的一些观点进行汇总,供大家讨论:

  • 为什么需要单独的进程内核栈?

    • 所有进程运行的时候,都可能通过系统调用陷入内核态继续执行。假设第一个进程 A 陷入内核态执行的时候,需要等待读取网卡的数据,主动调用 schedule() 让出 CPU;此时调度器唤醒了另一个进程 B,碰巧进程 B 也需要系统调用进入内核态。那问题就来了,如果内核栈只有一个,那进程 B 进入内核态的时候产生的压栈操作,必然会破坏掉进程 A 已有的内核栈数据;一但进程 A 的内核栈数据被破坏,很可能导致进程 A 的内核态无法正确返回到对应的用户态了;
  • 为什么需要单独的线程栈?

    • Linux 调度程序中并没有区分线程和进程,当调度程序需要唤醒”进程”的时候,必然需要恢复进程的上下文环境,也就是进程栈;但是线程和父进程完全共享一份地址空间,如果栈也用同一个那就会遇到以下问题。假如进程的栈指针初始值为 0x7ffc80000000;父进程 A 先执行,调用了一些函数后栈指针 esp 为 0x7ffc8000FF00,此时父进程主动休眠了;接着调度器唤醒子线程 A1: 此时 A1 的栈指针 esp 如果为初始值 0x7ffc80000000,则线程 A1 一但出现函数调用,必然会破坏父进程 A 已入栈的数据。如果此时线程 A1 的栈指针和父进程最后更新的值一致,esp 为 0x7ffc8000FF00,那线程 A1 进行一些函数调用后,栈指针 esp 增加到 0x7ffc8000FFFF,然后线程 A1 休眠;调度器再次换成父进程 A 执行,那这个时候父进程的栈指针是应该为 0x7ffc8000FF00 还是 0x7ffc8000FFFF 呢?无论栈指针被设置到哪个值,都会有问题不是吗?
  • 进程和线程是否共享一个内核栈?

    • No,线程和进程创建的时候都调用 dup_task_struct 来创建 task 相关结构体,而内核栈也是在此函数中 alloc_thread_info_node 出来的。因此虽然线程和进程共享一个地址空间 mm_struct,但是并不共享一个内核栈。
  • 为什么需要单独中断栈?

    • ARM在FIR/IRQ中断发生后,切换到SVC模式的过程中,包括ABT异常的过程中需要中断栈来保存数据,切换到SVC模式后,就用的是用户进程的内核栈了
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值