C程序函数调用&系统调用

· 浏览次数 : 0

小编点评

计算机程序的执行过程可以分为以下几个步骤: 1. 二进制文件的加载:程序作为一个二进制文件,其中包含代码指令、文本信息等。将这个二进制文件加载到内存中,使得程序的代码(以汇编指令表示)被记载到内存中。 2. CPU的执行:程序加载到内存后,CPU从main函数处的地址开始执行。CPU在执行过程中会不断地读取指令,然后逐条执行。在执行过程中,可能发生跳转(例如条件语句)、操作栈(例如函数的局部变量存储)等操作。 3. 系统调用:程序在执行过程中可能需要执行系统调用,例如文件读写、网络通信等。现代操作系统将这些功能放在内核态执行,而普通用户程序只可以通过系统调用来执行这些功能。 4. 用户态与内核态的切换:CPU的执行过程涉及用户态和内核态之间的切换。操作系统内核负责处理各种功能,而普通用户程序不能直接参与这些操作。因此,当程序需要进行系统调用时,CPU会从用户态切换到内核态;反之,当系统调用完成后,CPU会从内核态切换回用户态。 5. 特权级别的切换:操作系统利用CPU的特权级机制来实现内核态与用户态之间的切换,以保证系统的安全性和稳定性。特权级切换可以通过系统调用、中断/异常处理等方式实现。此外,x86架构提供了特殊的特权级切换指令,如call gate、task gate等。 综上所述,计算机程序的执行过程涉及多个阶段,包括二进制文件的加载、CPU的执行、系统调用的执行、用户态与内核态的切换以及特权级的切换。这些步骤共同构成了程序执行的整个过程。

正文

理解程序的执行

我们要知道CPU可以自由地访问寄存器、内存。另外,程序是由操作系统执行的,所以操作系统能够控制程序的所有执行情况,限制程序的行为。

程序地执行过程:

  • 程序是一个二进制文件,包含程序的代码指令、代码中的文本信息等(参考C语言的程序的各种段)
  • 执行一个程序后,会将这个二进制加载到内存中,那么这个程序的代码(想象成各种汇编指令)也就记载道了内存中
  • CPU执行程序时从固定的位置main处开始执行(eip寄存器指向这里),逐条语句读取执行(这是CPU自带的功能)
    • 语句可能发生跳转(eip切换到其他汇编指令出)
    • 语句可能会操作栈(其实就是往一块特殊地内存空间写入数据、读出数据,CPU有相关的指令pop push解决这个问题)
  • 程序可能会执行系统调用(操作系统赋予的能力,例如读写文件,网络通信等)。现代操作系统将这些能力都放到了内核态来执行了,即只有内核代码才能做实际的读写文件操作,普通用户程序只能通过系统调用来执行这些能力。
    • 所以执行系统调用后,cpu就会相应地跳转到系统调用地入口处(这个系统调用的入口也时固定的,对应的是内核中的一段C代码
    • 内核的系统调用入口函数,根据系统调用号(对每个系统调用的标识),找到相应的处理函数执行(其实也是执行call函数)
    • 系统调用处理完后,继续返回到用户自己的程序代码处执行(所以,在执行系统调用前需要把用户代码执行的位置记录下来,并且在系统调用结束后自动设置eip指向这个地方)

函数调用

C语言函数调用关键

c语言函数调用的几个关键点在于:

  • 保护调用者的上下文(寄存器、栈指针(ebp,esp)信息)
  • 将传入参数通过esi、edi等放到被寄存器中、或者push到栈中(当参数比较多时)
  • 执行call调用函数,call的副作用是将eip压入到栈中
  • 将计算的返回值放到eax中
  • pop出ebp、esp
  • 执行ret,将eip从栈中pop出来,然后指令继续执行重新回到调用者上下文(将esp指向调用者调用函数后的语句)

系统调用

  • syscall sysenter sysret
  • int 0x80

在 x86-64 架构上,当应用程序需要执行系统调用时,CPU 会从用户态切换到内核态,经历以下过程:

  1. 用户态程序执行 syscall 指令:
    • 用户态程序通过执行 syscall 指令来触发系统调用请求。
  2. CPU 切换到内核态:
    • syscall 指令会引发一个特殊的异常,导致 CPU 从当前的用户态特权级切换到内核态的更高特权级。
    • 这个过程会自动保存用户态的部分寄存器状态,如 riprflags 等,并将控制权转交给内核。
  3. 内核处理系统调用:
    • 内核接管控制权后,会根据系统调用号找到对应的系统调用处理函数,并执行相应的操作。
    • 内核执行完成后,会将结果返回给用户态程序。
  4. 从内核态切换回用户态:
    • 内核执行完系统调用处理后,会通过 sysret 指令从内核态切换回用户态。
    • sysret 指令会自动恢复之前保存的用户态寄存器状态,并将控制权转回给用户态程序。

整个切换过程由硬件和操作系统内核共同完成,应用程序感知不到这个切换过程。这种基于硬件支持的用户态 - 内核态切换机制,能够大幅降低系统调用的开销,提高操作系统的整体性能。
需要注意的是,除了 syscall/sysret 指令,Intel 的 x86-64 架构也支持使用 int 0x80 软中断来执行系统调用,不过 syscall/sysret 方式通常更加高效。

linux系统调用 - chenjx_ucs - 博客园 (cnblogs.com)

cpu的特权级别(privilege level )

chatgpt告诉我们:Linux 系统通过以下几种方式来实现当前特权级别的切换:

  1. 系统调用:
    • 用户态应用程序通过系统调用机制从 Ring 3 切换到 Ring 0 内核态。
    • 应用程序执行 int 0x80 或 syscall 指令触发软中断,CPU 切换到 Ring 0 执行内核代码。
    • 内核处理完成后,通过 iret 或 sysret 指令返回到用户态。
  2. 中断/异常处理:
    • 当 CPU 遇到硬件中断或软件异常时, 会自动从当前特权级切换到 Ring 0 内核态。
    • 内核处理完中断/异常后,通过 iret 指令返回到之前的特权级别。
  3. 特权级切换指令:
    • x86 架构提供了一些用于特权级切换的指令,如 call gatetask gate 等。
    • 这些指令可以在不同特权级之间跳转,并自动完成上下文切换。
  4. 进程切换:
    • 当内核需要切换进程时,会切换进程的特权级别。
    • 内核将新进程的特权级别设置为 Ring 3,并通过 iret 指令返回到用户态。

在 Linux 中,大多数情况下都是通过系统调用和中断/异常处理来实现特权级切换。内核代码运行在 Ring 0 级别,用户态应用程序运行在 Ring 3 级别。当应用程序需要访问受保护的系统资源时,会通过系统调用陷入内核态,由内核代码执行相应的操作。中断和异常处理也会触发内核态的切换,内核负责处理各种硬件事件。总之,Linux 系统利用 CPU 硬件提供的特权级机制,通过系统调用、中断/异常处理、特权级切换指令等方式,实现了内核态和用户态之间的特权级切换,保证了系统的安全和稳定性。

Ring 0和Ring 3也有其他区别,例如Ring 0 程序可以执行所有的 CPU 指令集,包括特权指令。Ring 3 程序只能执行非特权指令集,无法直接执行特权级别的指令。

参考资料:
用户空间与内核空间,进程上下文与中断上下文[总结] - Rabbit_Dale - 博客园 (cnblogs.com)

与C程序函数调用&系统调用相似的内容:

C程序函数调用&系统调用

理解程序的执行 我们要知道CPU可以自由地访问寄存器、内存。另外,程序是由操作系统执行的,所以操作系统能够控制程序的所有执行情况,限制程序的行为。 程序地执行过程: 程序是一个二进制文件,包含程序的代码指令、代码中的文本信息等(参考C语言的程序的各种段) 执行一个程序后,会将这个二进制加载到内存中,

[转帖]「理解C++20协程原理」从Linux线程、线程与异步编程、协程与异步

协程不是系统级线程,很多时候协程被称为“轻量级线程”、“微线程”、“纤程(fiber)”等。简单来说可以认为协程是线程里不同的函数,这些函数之间可以相互快速切换。 协程和用户态线程非常接近,用户态线程之间的切换不需要陷入内核,但部分操作系统中用户态线程的切换需要内核态线程的辅助。 协程是编程语言(或

python入门基础(13)--类、对象、全局函数,类内部调用

面向过程的编程语言,如C语言,所使用的数据和函数之间是没有任何直接联系的,它们之间是通过函数调用提供参数的形式将数据传入函数进行处理。 但可能因为错误的传递参数、错误地修改了数据而导致程序出错,甚至是崩溃。当需要修改或维护程序时要从程序提供的一堆数据中去寻找和修改它,要扩展函数的功能,只能重新建立一

C++如何在main函数开始之前(或结束之后)执行一段逻辑?

1. 问题 2. 考察的要点 3. 解决策略 3.1. 方案一:使用GCC的拓展功能 3.2. 方案二:使用全局变量 3.3. 方案三:atexit 4. Demo测试 4.1. 测试代码 4.2. 执行结果 5. 程序异常退出场景 5.1. 存在的问题 5.2. 解决方案 5.2.1. 原理 5.

整理C语言预处理过程语法的实用方法与技巧

预处理 目录预处理一、宏定义数值宏常量字符串宏常量用define宏定义注释符号?程序的编译过程预处理中宏替换和去注释谁先谁后?如何写一个不会出现问题的宏函数do-while-zero结构do-while-zero的评价宏定义中的空格宏只能在main函数上面定义吗?宏的作用范围#undef宏替换是在函

golang的 CGO 是什么

CGO是Go(Golang)语言中的一个工具,全称为 "C-Go" 或者 "C for Go"。 它是Go标准库的一部分,允许Go代码与C语言代码进行交互。 CGO提供了在Go程序中使用C语言库的能力,同时也允许C代码调用Go的函数。 通过CGO,开发者可以利用Go语言的强类型和垃圾回收等特性,同时

Lua热更学习--使用toLua中的协程

[6] C#访问调table类中的成员变量和函数 访问table中的变量和函数 lua中可以使用table作为class,因此对table中的函数访问调用是必要的根据前面对table访问和function的获取调用,这里尝试获取调用。 依然是如此,此种调用方式获取到的table中的函数是引用拷贝。

C# 程序集、模块和类型概念及关系

目录C# 程序集、模块和类型概念及关系概述程序集模块类型程序集、模块和类型的关系总结引用 C# 程序集、模块和类型概念及关系 概述 在 C# 中,程序集、模块和类型是构成 .NET 应用程序的基本单元。它们之间具有以下层次关系: 程序集 是包含 .NET 代码的单元,它可以是一个可执行文件(EXE)

WPF/C#:程序关闭的三种模式

ShutdownMode枚举类型介绍 ShutdownMode是一个枚举类型,它定义了WPF应用程序的关闭方式。这个枚举类型有三个成员: OnLastWindowClose:当最后一个窗口关闭或者调用System.Windows.Application.Shutdown方法时,应用程序会关闭。 On

.NET C# 程序自动更新组件

引言 本来博主想偷懒使用AutoUpdater.NET组件,但由于博主项目有些特殊性和它的功能过于多,于是博主自己实现一个轻量级独立自动更新组件,可稍作修改集成到大家自己项目中,比如:WPF/Winform/Windows服务。大致思路:发现更新后,从网络上下载更新包并进行解压,同时在 WinFor