当前位置: 首页 > news >正文

深入汇编底层与操作系统系统调用接口:彻底掰开揉碎c语言简单的一行代码-打印helloworld是如何从C语言点击运行到显示在屏幕上的

有没有想过c语言最简单的一行代码:

int a [10];

a[0] = 1;
printf("%d \n",a[0]);

这个最简单的代码 是如何从点击运行到显示在屏幕上的????

今天继续深入分析问题,深入汇编与系统底层接口>>>>>

一、C 程序的执行流程概述

当你在 VS Code 中按下 Ctrl+Shift+N(假设这是你设置的运行快捷键)运行以下这段简单的 C 程序时:

int a[10];

a[0] = 1;

printf("%d \n",a[0]);

printf("hello world \n");

计算机内部经历了一系列复杂但有序的过程,从源代码到最终在屏幕上显示输出。这个过程可以分为编译阶段加载与执行阶段输出显示阶段三个主要部分。让我们逐步深入探讨每个阶段的具体实现。

1.1 编译阶段:从 C 代码到机器码

C 语言是一种高级编程语言,计算机不能直接理解和执行 C 代码。因此,必须通过编译器将 C 代码转换为计算机能够执行的机器码。这个过程可以分为四个子阶段:预处理、编译、汇编和链接。

1.1.1 预处理阶段

预处理是编译的第一个阶段,预处理器(Preprocessor)会处理源代码中的预处理指令,如#include和#define等。在这个阶段,预处理器会完成以下工作:

  1. 宏展开:将所有的宏定义展开,即用宏体替换程序中出现的宏名。
  1. 文件包含:处理#include指令,将指定的头文件内容插入到当前源文件中。
  1. 条件编译:根据#if、#ifdef等条件编译指令,确定哪些代码应该被包含在最终的编译结果中。
  1. 注释删除:移除源代码中的所有注释。

对于我们的示例代码,预处理器会处理stdio.h头文件的包含,将其内容插入到源文件中。这个阶段的输出是一个经过处理的中间文件(通常以.i为扩展名),其中不再包含任何预处理指令。

1.1.2 编译阶段

编译阶段是编译器的核心工作阶段,它将预处理后的代码转换为汇编语言代码。在这个阶段,编译器会执行以下步骤:

  1. 词法分析:将源代码分解为一个个词法单元(Token),如关键字、标识符、常量、运算符等。
  1. 语法分析:根据 C 语言的语法规则,将词法单元组合成语法树,检查语法结构的正确性。
  1. 语义分析:检查代码的语义正确性,如类型匹配、变量声明等。
  1. 中间代码生成:将语法树转换为中间表示形式,通常是一种与机器无关的中间代码。
  1. 优化:对中间代码进行优化,以提高程序的执行效率。
  1. 目标代码生成:将优化后的中间代码转换为特定机器架构的汇编代码。

对于我们的示例代码,编译器会将 C 语句转换为对应的汇编指令,例如将a[0] = 1转换为将数值 1 存储到数组 a 的第一个元素的位置的汇编指令。

1.1.3 汇编阶段

汇编阶段是将编译阶段生成的汇编代码转换为机器码(二进制指令)的过程。这个任务由汇编器(Assembler)完成。汇编器将每条汇编指令翻译成对应的机器码,并生成目标文件(在 Windows 系统下通常为.obj文件)。

汇编器的工作相对简单直接,它只需要根据汇编指令和机器指令对照表进行一一翻译,不需要进行任何优化。每条汇编指令对应一条或多条机器指令,具体取决于指令的复杂程度。

1.1.4 链接阶段

链接阶段是将多个目标文件(包括可能引用的库文件)组合成一个可执行文件的过程。这个任务由链接器(Linker)完成。链接器的主要工作包括:

  1. 符号解析:将目标文件中使用的符号(如函数名、变量名)与其定义进行匹配。
  1. 重定位:调整目标代码中的地址,使其指向正确的内存位置。
  1. 库文件链接:将程序中使用的库函数与目标代码进行链接。

在链接阶段,链接器会将printf函数的实现(通常位于 C 标准库中)与我们的程序代码进行链接,使得程序在运行时能够正确调用printf函数。

1.2 加载与执行阶段:从可执行文件到内存中的程序

当程序被编译链接成可执行文件后,就可以被执行了。在 Windows 系统中,当你在 VS Code 中按下运行快捷键(假设是 Ctrl+Shift+N)时,系统会启动一个新的进程来执行该程序。

1.2.1 程序加载

程序加载是指将可执行文件的内容从磁盘读取到内存中的过程。Windows 操作系统的加载器(Loader)负责完成这一任务。加载器会:

  1. 为程序分配内存空间,包括代码段、数据段、堆和栈等。
  1. 将可执行文件中的代码和数据复制到内存中的相应位置。
  1. 设置程序的初始执行环境,如寄存器初始值、栈指针等。

在这个过程中,程序的全局变量(如示例中的数组a)会被初始化到内存中的数据段。对于未显式初始化的数组元素,C 语言标准规定它们将被初始化为 0。

1.2.2 程序执行

程序加载完成后,操作系统会将控制权交给程序的入口点(通常是main函数),程序开始执行。在执行过程中,CPU 会按照指令指针(IP,Instruction Pointer)的指示,逐条读取并执行指令。

对于我们的示例程序,执行过程大致如下:

  1. 程序启动,执行数组a的初始化(虽然数组声明时未显式初始化,但 C 语言保证全局数组会被初始化为 0)。
  1. 将数值 1 存储到数组a的第一个元素(a[0])中。
  1. 调用printf函数输出a[0]的值。
  1. 再次调用printf函数输出 "hello world"。
  1. 程序执行结束,返回操作系统。

二、C 代码到汇编代码的转换详解

2.1 示例代码的汇编表示

让我们将示例 C 代码转换为对应的汇编代码,以 x86-64 架构为例(Windows 系统下的常见架构)。以下是示例代码的简化汇编表示:

section .data

a dd 0,0,0,0,0,0,0,0,0,0 ; 定义一个包含10个双字(4字节)的数组,初始化为0

section .text

global main

main:

; 设置栈帧

push rbp

mov rbp, rsp

; 将1存储到数组a的第一个元素

mov DWORD PTR [a], 1

; 调用printf输出a[0]

mov esi, [a] ; 将a[0]的值放入esi寄存器(对应printf的%d参数)

lea rdi, [fmt1] ; 将格式字符串地址放入rdi寄存器

call printf

; 调用printf输出"hello world"

lea rdi, [fmt2] ; 将格式字符串地址放入rdi寄存器

call printf

; 清理栈帧并返回

mov rsp, rbp

pop rbp

ret

section .data

fmt1 db "%d \n", 0 ; 格式字符串1

fmt2 db "hello world \n", 0 ; 格式字符串2

2.2 关键汇编指令解析

2.2.1 数据定义指令
  • dd:定义双字(4 字节)数据。在.data段中,a dd 0,0,0,0,0,0,0,0,0,0定义了一个包含 10 个双字的数组a,初始值均为 0。
  • .db:定义字节数据。fmt1 db "%d \n", 0定义了一个以空字符结尾的字符串。
2.2.2 寄存器操作指令
  • mov:数据传送指令。mov DWORD PTR [a], 1将数值 1 存储到数组a的第一个元素;mov esi, [a]将a[0]的值放入esi寄存器。
  • lea:加载有效地址。lea rdi, [fmt1]将格式字符串fmt1的地址加载到rdi寄存器中。
2.2.3 函数调用指令
  • call:调用函数指令。call printf调用printf函数。
  • ret:从函数返回指令。ret指令会从栈中弹出返回地址,并跳转到该地址继续执行。
2.2.4 栈操作指令
  • push:将数据压入栈。push rbp将基址寄存器rbp的值压入栈中。
  • pop:从栈中弹出数据。pop rbp将栈顶的值弹出到rbp寄存器中。
  • rsp:栈指针寄存器,始终指向栈顶。

2.3 数组操作的底层实现

在 C 语言中,数组访问a[0]在汇编层面被转换为对内存地址的直接操作。对于全局数组a,其地址在编译时是已知的,因此可以直接使用绝对地址进行访问。

在 x86-64 架构下,数组元素的访问方式如下:

  • 数组名a代表数组的起始地址。
  • 数组元素a[i]的地址计算为a + i * sizeof(type)。
  • 对于int类型数组(假设为 4 字节),a[0]的地址就是a,a[1]的地址是a + 4,依此类推。

在示例代码中,a[0] = 1被转换为mov DWORD PTR [a], 1,即将双字(4 字节)数值 1 存储到地址a处。

三、printf 函数的底层实现分析

3.1 printf 函数的调用过程

在 C 语言中,printf是一个可变参数函数,其原型为:

int printf(const char *format, ...);

当调用printf时,参数按照从右到左的顺序压入栈中。对于示例中的第一个printf调用:

printf("%d \n", a[0]);

其参数压栈顺序为:

  1. 将a[0]的值压入栈中(对应%d参数)。
  1. 将格式字符串"%d \n"的地址压入栈中。
  1. 调用printf函数。

在 x86-64 架构的 Windows 系统中,函数参数的传递方式有所不同。前四个整数或指针参数通过寄存器rcx、rdx、r8和r9传递,超过四个的参数通过栈传递。因此,对于printf函数的调用,参数传递方式为:

  1. 将格式字符串的地址放入rdi寄存器。
  1. 将第一个参数(a[0]的值)放入rsi寄存器。
  1. 后续参数依次放入rdx、rcx、r8、r9寄存器,超过六个的参数通过栈传递。

3.2 printf 到系统调用的转换

printf函数最终会通过系统调用将数据输出到控制台。在 Windows 系统中,这一过程涉及以下步骤:

  1. 格式化处理:printf函数首先解析格式字符串,将各种类型的数据转换为对应的字符串表示。
  1. 缓冲区管理:将格式化后的字符串写入缓冲区。
  1. 刷新缓冲区:当缓冲区满或遇到换行符时,将缓冲区内容输出到控制台。
  1. 系统调用:通过WriteConsoleA函数将数据写入控制台。

WriteConsoleA是 Windows API 中的一个函数,用于向控制台写入字符。其原型为:

BOOL WINAPI WriteConsoleA(

_In_ HANDLE hConsoleOutput,

_In_ LPCVOID lpBuffer,

_In_ DWORD nNumberOfCharsToWrite,

_Out_opt_ LPDWORD lpNumberOfCharsWritten,

_Reserved_ LPVOID lpReserved

);

参数说明:

  • hConsoleOutput:控制台输出句柄。
  • lpBuffer:指向要写入的缓冲区的指针。
  • nNumberOfCharsToWrite:要写入的字符数。
  • lpNumberOfCharsWritten:指向接收实际写入字符数的变量的指针。
  • lpReserved:保留参数,必须为NULL。

3.3 汇编代码中的系统调用实现

在汇编层面,调用WriteConsoleA函数的过程如下:

  1. 准备函数参数:
    • 将hConsoleOutput(通常是标准输出句柄)放入rdi寄存器。
    • 将lpBuffer(指向字符串的指针)放入rsi寄存器。
    • 将nNumberOfCharsToWrite(字符数)放入rdx寄存器。
    • 将lpNumberOfCharsWritten(接收写入字符数的变量地址)放入rcx寄存器。
    • 将lpReserved(通常为NULL)放入r8寄存器。
  1. 调用WriteConsoleA函数:

call WriteConsoleA

  1. 检查返回值(可选):
    • 如果WriteConsoleA返回TRUE(非零值),表示写入成功。
    • 如果返回FALSE(零值),表示写入失败,可以通过GetLastError获取错误代码。

在示例代码中,printf函数内部最终会调用WriteConsoleA来输出字符串。对于第二个printf调用:

printf("hello world \n");

printf会将字符串 "hello world \n" 写入缓冲区,然后调用WriteConsoleA将其输出到控制台。

四、机器码的执行过程

4.1 机器码的结构与执行流程

机器码是 CPU 可以直接执行的二进制指令。每条机器码由操作码(Opcode)和操作数(Operand)组成。操作码指定要执行的操作,操作数指定操作的对象。

在 x86 架构中,机器码的长度可变,从 1 字节到 15 字节不等。例如,mov eax, 1的机器码是B8 01 00 00 00(5 字节),而add eax, ebx的机器码是03 C3(2 字节)。

CPU 执行机器码的过程可以分为以下几个阶段:

  1. 取指(Fetch):从内存中读取下一条指令,放入指令寄存器。
  1. 译码(Decode):分析指令的操作码和操作数,确定要执行的操作。
  1. 执行(Execute):执行指令指定的操作,可能涉及算术运算、逻辑运算、内存访问等。
  1. 访存(Memory Access):如果指令需要读写内存,执行相应的内存操作。
  1. 写回(Write Back):将执行结果写回寄存器或内存。
  1. 更新程序计数器(PC):指向下一条要执行的指令。

这一过程不断重复,直到遇到ret指令或程序结束。

4.2 示例代码的机器码分析

以下是示例代码中关键部分的机器码分析:

  1. 数组初始化

a dd 0,0,0,0,0,0,0,0,0,0

在内存中表示为 10 个连续的双字(4 字节),每个双字的值为 0。

  1. a[0] = 1

mov DWORD PTR [a], 1

机器码为C7 05 [offset] 01 00 00 00,其中[offset]是a的地址相对于当前指令的偏移量。这条指令将双字数值 1 存储到地址a处。

  1. 调用printf输出a[0]

mov esi, [a]

lea rdi, [fmt1]

call printf

对应的机器码可能为:

8B 35 [offset] ; mov esi, [a]

48 8D 3D [offset] ; lea rdi, [fmt1]

E8 [offset] ; call printf

这些指令将a[0]的值放入esi寄存器,将格式字符串的地址放入rdi寄存器,然后调用printf函数。

  1. 调用printf输出 "hello world"

lea rdi, [fmt2]

call printf

对应的机器码可能为:

48 8D 3D [offset] ; lea rdi, [fmt2]

E8 [offset] ; call printf

这些指令将格式字符串的地址放入rdi寄存器,然后调用printf函数。

4.3 寄存器的作用与变化

在程序执行过程中,各种寄存器扮演着不同的角色:

  • 指令指针(IP):始终指向当前正在执行的指令的下一条指令。
  • 栈指针(RSP):指向栈顶,用于管理函数调用和局部变量。
  • 基址寄存器(RBP):用于访问栈中的参数和局部变量。
  • 通用寄存器(RAX、RBX、RCX、RDX、RSI、RDI 等):用于存储临时数据和函数参数。

在示例程序的执行过程中,寄存器的变化如下:

  1. 程序开始时,RSP指向栈顶,RBP通常初始化为RSP的值。
  1. 执行mov DWORD PTR [a], 1时,RAX或其他寄存器可能被用来存储数值 1,然后存储到内存地址a处。
  1. 调用printf时,参数被放入RDI、RSI等寄存器,RSP可能会被调整以适应栈的变化。
  1. 函数返回后,RSP和RBP会被恢复,程序继续执行下一条指令。

五、从代码到屏幕的完整路径

5.1 输出流的处理机制

在 C 语言中,输出操作通常通过标准输出流(stdout)进行。printf函数将数据写入stdout流,该流可以被重定向到不同的输出设备,如控制台、文件或管道。

stdout流的输出过程如下:

  1. 缓冲区管理:stdout通常使用行缓冲或全缓冲机制。对于行缓冲,当遇到换行符(\n)时,缓冲区会被自动刷新。对于全缓冲,当缓冲区填满时会自动刷新。
  1. 流刷新:可以通过fflush函数手动刷新缓冲区。
  1. 底层写入:最终,数据会通过系统调用写入输出设备。

在 Windows 系统中,stdout通常对应控制台窗口。当程序向stdout写入数据时,最终会通过WriteConsoleA函数将数据输出到控制台屏幕缓冲区。

5.2 控制台 API 的工作原理

Windows 控制台 API 提供了一系列函数来操作控制台窗口和屏幕缓冲区。其中,WriteConsoleA是用于向控制台写入字符的核心函数。

WriteConsoleA的工作原理如下:

  1. 获取控制台句柄:通过GetStdHandle函数获取标准输出句柄(STD_OUTPUT_HANDLE)。
  1. 准备参数:指定要写入的字符缓冲区、字符数、接收实际写入字符数的变量等。
  1. 调用WriteConsoleA:将字符写入控制台屏幕缓冲区。
  1. 更新光标位置:WriteConsoleA会自动更新控制台光标的位置,使其位于最后一个写入字符之后。

在示例程序中,第二个printf调用输出 "hello world \n" 时,printf会将字符串写入stdout缓冲区,然后调用WriteConsoleA将其输出到控制台。WriteConsoleA会将字符串写入控制台屏幕缓冲区,并将光标移动到下一行的开头。

5.3 字符在屏幕上的显示过程

当WriteConsoleA将字符写入控制台屏幕缓冲区后,这些字符并不会立即显示在屏幕上。控制台驱动程序会定期检查屏幕缓冲区的变化,并将其渲染到屏幕上。

字符在屏幕上的显示过程如下:

  1. 屏幕缓冲区更新:WriteConsoleA将字符写入控制台屏幕缓冲区的指定位置,包括字符本身和属性(如颜色、背景色等)。
  1. 控制台窗口重绘:控制台驱动程序检测到屏幕缓冲区的变化后,会更新对应的屏幕区域。
  1. 显示输出:最终,字符会显示在控制台窗口的指定位置。

在示例程序中,两个printf调用会导致两次WriteConsoleA调用,分别输出 "1" 和 "hello world"(假设换行符\n也被处理)。控制台会将这些字符显示在屏幕上,通常是在程序运行窗口的当前光标位置。

六、在 VS Code 中查看底层执行

6.1 配置 VS Code 查看汇编代码

要在 VS Code 中查看 C 代码对应的汇编代码,可以按照以下步骤进行配置:

  1. 安装 C/C++ 扩展:确保已安装 Microsoft 官方的 C/C++ 扩展。
  1. 创建或修改launch.json:在.vscode文件夹中创建或修改launch.json文件,添加以下配置:

{

"version": "0.2.0",

"configurations": [

{

"name": "(gdb) Launch",

"type": "cppdbg",

"request": "launch",

"program": "${fileDirname}/${fileBasenameNoExtension}",

"args": [],

"stopAtEntry": false,

"cwd": "${workspaceFolder}",

"environment": [],

"externalConsole": true,

"MIMode": "gdb",

"showDisassembly": "always" // 添加此行以始终显示反汇编视图

}

]

}

  1. 编译并调试:使用Ctrl+Shift+B编译程序,然后使用F5启动调试。

6.2 使用调试工具查看机器码

在调试过程中,可以使用 VS Code 的调试工具查看机器码:

  1. 打开反汇编窗口:在调试过程中,选择Debug > Windows > Disassembly或按Ctrl+Alt+D打开反汇编窗口。
  1. 查看机器码:反汇编窗口会显示当前执行的汇编指令及其对应的机器码。
  1. 单步执行:使用F10(逐过程)或F11(逐语句)单步执行程序,观察寄存器和内存的变化。
  1. 查看寄存器:使用Debug > Windows > Registers打开寄存器窗口,查看寄存器的值。
  1. 查看内存:使用Debug > Windows > Memory打开内存窗口,查看内存中的数据。

在反汇编窗口中,每条汇编指令旁边会显示对应的机器码。例如,mov eax, 1的机器码可能显示为B8 01 00 00 00。

6.3 实际操作示例

以下是在 VS Code 中查看示例程序底层执行的实际操作步骤:

  1. 编写示例代码:在 VS Code 中创建名为example.c的文件,输入示例代码。
  1. 配置调试:按照上述步骤配置launch.json。
  1. 编译程序:按Ctrl+Shift+B编译程序。
  1. 设置断点:在要查看的代码行旁边单击,设置断点。
  1. 启动调试:按F5启动调试,程序会在第一个断点处暂停。
  1. 查看反汇编:打开反汇编窗口,查看当前执行的汇编指令和机器码。
  1. 单步执行:按F10或F11单步执行程序,观察寄存器和内存的变化。
  1. 查看输出:程序执行结束后,查看控制台窗口中的输出结果。

通过这种方式,可以直观地看到 C 代码如何被转换为汇编代码和机器码,以及程序在执行过程中寄存器和内存的变化。

七、总结与扩展思考

7.1 关键知识点回顾

通过对示例 C 程序的深入分析,我们学习了以下关键知识点:

  1. C 程序的执行流程:从源代码到可执行文件,再到内存中的程序执行,涉及编译、链接、加载和执行等多个阶段。
  1. 汇编代码与机器码:C 代码被编译为汇编代码,然后被汇编为机器码。汇编代码提供了对底层操作的抽象,而机器码是 CPU 可以直接执行的二进制指令。
  1. 函数调用机制:函数参数的传递方式、栈的使用以及返回值的处理。
  1. 输出机制:printf函数如何将数据输出到控制台,以及底层的系统调用实现。
  1. 调试工具使用:在 VS Code 中查看汇编代码和机器码,以及使用调试工具分析程序执行过程。

7.2 进一步学习建议

如果你希望进一步深入了解 C 程序的底层执行机制,可以考虑以下学习方向:

  1. 学习汇编语言:掌握 x86 或 ARM 汇编语言,这将帮助你更好地理解 C 代码的底层实现。
  1. 研究不同的调用约定:了解__cdecl、__stdcall、__fastcall等不同调用约定的区别和应用场景。
  1. 探索操作系统 API:学习 Windows API 或 Linux 系统调用,了解 C 标准库函数如何与操作系统交互。
  1. 研究编译器优化:了解编译器如何优化代码,以及不同优化选项对生成的机器码的影响。
  1. 学习调试技巧:掌握更高级的调试技巧,如内存查看、寄存器分析、条件断点等。

7.3 扩展思考问题

以下是一些可以帮助你进一步理解底层执行机制的思考问题:

  1. 缓冲区溢出是如何发生的?
    • 当向缓冲区写入超过其容量的数据时,会导致缓冲区溢出,可能覆盖相邻的内存区域,包括函数返回地址,从而导致程序崩溃或安全漏洞。
  1. 为什么printf能接受可变数量的参数?
    • printf利用了 C 语言的可变参数机制,通过va_list和相关宏来访问可变参数列表。在底层,参数通过栈或寄存器传递,可以动态访问。
  1. 不同操作系统下的输出机制有何不同?
    • 在 Windows 系统中,输出通过WriteConsoleA等 API 实现;在 Linux 系统中,输出通过write系统调用实现。两者的参数和调用方式有所不同。
  1. 程序如何处理错误?
    • 系统调用通常返回错误码,可以通过GetLastError(Windows)或errno(Linux)获取错误信息,程序可以根据这些信息进行错误处理。
  1. 为什么需要链接阶段?
    • 链接阶段将多个目标文件和库文件组合成一个可执行文件,解决符号引用问题,使程序能够正确调用外部函数和变量。

通过思考这些问题并寻找答案,你将对 C 程序的底层执行机制有更深入的理解。

八、附录:示例代码的完整汇编与机器码

以下是示例 C 程序的完整汇编代码和对应的机器码(x86-64 架构,Windows 系统):

8.1 完整汇编代码

.386

.model flat, stdcall

.stack 4096

.data

a dd 0,0,0,0,0,0,0,0,0,0 ; 数组a,初始化为0

fmt1 db "%d \n", 0 ; 格式字符串1

fmt2 db "hello world \n", 0 ; 格式字符串2

.code

main proc

; 保存基址寄存器

push ebp

mov ebp, esp

; 将1存储到数组a的第一个元素

mov DWORD PTR [a], 1

; 调用printf输出a[0]

mov eax, [a] ; 将a[0]的值放入eax

push eax ; 压入%参数

push offset fmt1 ; 压入格式字符串地址

call printf ; 调用printf

add esp, 8 ; 清理栈(2个参数,每个4字节)

; 调用printf输出"hello world"

push offset fmt2 ; 压入格式字符串地址

call printf ; 调用printf

add esp, 4 ; 清理栈(1个参数)

; 恢复基址寄存器并返回

mov esp, ebp

pop ebp

ret 0

main endp

; 引入printf函数

extern printf:PROC

END

8.2 关键机器码分析

以下是关键汇编指令对应的机器码:

  1. mov DWORD PTR [a], 1
    • 机器码:C7 05 [offset] 01 00 00 00
    • 说明:将双字数值 1 存储到地址a处。
  1. mov eax, [a]
    • 机器码:A1 [offset]
    • 说明:将地址a处的双字值加载到eax寄存器。
  1. push eax
    • 机器码:50
    • 说明:将eax的值压入栈中。
  1. push offset fmt1
    • 机器码:68 [offset]
    • 说明:将格式字符串fmt1的地址压入栈中。
  1. call printf
    • 机器码:E8 [offset]
    • 说明:调用printf函数,offset是printf函数的相对地址。
  1. add esp, 8
    • 机器码:83 C4 08
    • 说明:清理栈,释放 8 字节的空间(两个 4 字节参数)。
  1. ret 0
    • 机器码:C3
    • 说明:从函数返回,0表示不清理栈(在stdcall调用约定中,函数本身负责清理栈)。

通过分析这些机器码,可以更深入地理解 C 程序在底层是如何被执行的。

如果你觉得写的还不错,请直接点赞+收藏+关注!!!!!


文章转载自:

http://5DZrcUA2.bgzgq.cn
http://sfm2UW8R.bgzgq.cn
http://0yxjHCp0.bgzgq.cn
http://jly4N47i.bgzgq.cn
http://5Cok9G4q.bgzgq.cn
http://G6K3WvR5.bgzgq.cn
http://vozlnjnd.bgzgq.cn
http://eYFzGehK.bgzgq.cn
http://rhZKXCVh.bgzgq.cn
http://dleOXOaR.bgzgq.cn
http://EqtSXkfc.bgzgq.cn
http://oqRAby9d.bgzgq.cn
http://zcJAGwp3.bgzgq.cn
http://M5QIk1SQ.bgzgq.cn
http://zEkyKbz1.bgzgq.cn
http://HRfQKj17.bgzgq.cn
http://vkAnaMlv.bgzgq.cn
http://fFJORAaP.bgzgq.cn
http://AknCJHp4.bgzgq.cn
http://QvUeQJBv.bgzgq.cn
http://IgKWQkYX.bgzgq.cn
http://mXBWpvV1.bgzgq.cn
http://HEn5enAp.bgzgq.cn
http://71LKTQaW.bgzgq.cn
http://1NZvA1D8.bgzgq.cn
http://6Xb0rLB0.bgzgq.cn
http://RhKXEKeG.bgzgq.cn
http://BtbTiy0q.bgzgq.cn
http://XmaJI49v.bgzgq.cn
http://l2JGLoyd.bgzgq.cn
http://www.dtcms.com/a/386026.html

相关文章:

  • ARM3.(汇编函数和c语言相互调用及ARM裸机开发环境搭建)
  • LeetCode 380 - O(1) 时间插入、删除和获取随机元素
  • 9 基于机器学习进行遥感影像参数反演-以随机森林为例
  • DB Hitek宣布推出650V GaN HEMT工艺
  • 机器学习简单数据分析案例
  • [特殊字符] 欢迎使用 C++ Arrow 函数 - 革命性的新特性!
  • 外网访问分布式跟踪系统 zipkin
  • Base 发币在即:L2 代币能否撬动生态增长?
  • DRDR生态Token正式上线BitMart,开启全球化新篇章
  • Spring Boot 3 + EasyExcel 文件导入导出实现
  • 9.16总结
  • Android开机时间查看
  • 探针水平的表达矩阵转换为基因水平的表达矩阵是芯片数据分析中关键的一步
  • PHP基础-语法初步(第七天)
  • 奥威BI与ChatBI:自然语言交互赋能企业数据分析新体验
  • Vue: 组件基础
  • 亚马逊云科技 EC2 服务终端节点:安全高效访问云服务的利器
  • 2026届计算机毕业设计选题 大数据毕业设计选题推荐 题目新颖 数据分析 可视化大屏 通过率高
  • html实现文字横向对齐以及margin的解释
  • 如何轻松找到并畅玩Edge浏览器隐藏的冲浪小游戏
  • K8S中的神秘任务Job与CronJob
  • go grpc开发使用
  • [论文阅读] 人工智能 + 软件工程 | 告别冗余HTML与高算力消耗:EfficientUICoder如何破解UI2Code的token难题
  • Golang语言入门篇004_Go命令详解
  • K8S的Pod状态处理指南
  • Gin框架:构建高性能Go Web应用
  • Golang中的NaN(Not a Number)
  • golang 做webrtc开发核心
  • Go语言中 error 接口与自定义错误类型的深入解析
  • D008 vue+django+neo4j基于知识图谱的政务服务搜索推荐系统