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

由浅入深学习 C 语言:Hello World【提高篇】

目录

引言

1. Hello World 程序代码

2. C 语言角度分析 Hello World 程序

2.1. 程序功能分析

2.2 指针

2.3 常量指针

2.4 指针常量

3. 反汇编角度分析 Hello World 程序

3.1 栈

3.2 函数用栈传递参数

3.3 函数调用栈

3.4 函数栈帧

3.5 相关寄存器

3.6 相关汇编指令

3.7 汇编代码分析

3.7.1 invoke_main() 函数调用了 main 函数

3.7.2 main 函数的栈帧建立和销毁过程

3.7.3 "Hello World\n" 字符串在内存中是以 assii 码的形式保存


引言

        本篇是 Hello World 程序提高篇,默认读者是有 C 语言编程基础,0 基础建议先阅读这篇博文的姐妹篇之由浅入深学习 C 语言:Hello World【基础篇】-CSDN博客

1. Hello World 程序代码

#include <stdio.h>   

int main(int argc, const char *argv[])
{
	for (int i = 0; i < argc; i++)    // 打印命令行参数
		printf("argv[%d] = %s\n", i, argv[i]);

	printf("Hello World\n");	

	return 0;        
}

2. C 语言角度分析 Hello World 程序

2.1. 程序功能分析

1.  #include <stdio.h>   // 预处理器指令,用于把 stdio.h 文件包含进来

2. int main(int argc, const char *argv[])        // 主函数,是 C 程序的入口函数

(1)函数名前的 int 是函数的返回值

(2)argc 是函数的第一个参数,参数类型是 int

  • 该参数表示的是命令行参数个数,传给这个参数的值是第二个参数 argv 数组的元素个数。
  • 一个 C 程序至少有一个命令行参数,这个命令行参数是 可执行文件的绝对路径名或相对路径名

(3)argv 是函数的第二个参数,参数类型是 const char* 数组,也就是说这个参数是一个数组,数组的每一项存的数据类型是 const char* (常量字符指针),指向每一个命令行参数字符串的首地址

比如,我们的 Hello World 程序,在 linux 平台下,生成 main.out 可执行文件。 

  • ./main.out  // 不带参数运行程序
  • argc = 1
  • argv 数组有 1 个元素,这个元素存储的就是 "./main.out" 这个字符串在内存中的首地址

  • ./main.out 111 222 // 带 2 个参数运行程序
  • argc = 3
  • argv 数组有 3 个元素,第一个元素存储的是 "./main.out" 这个字符串在内存中的首地址,第二个元素存储的是 "111" 这个字符串在内存的首地址,第三个元素存储的是 "222" 这个字符串在内存的首地址 

2.2 指针

        在 C 语言中,不管什么数据类型的指针,实际就是一块大小固定的内存,这块内存存的值另一块内存单元的地址,也就是说这块内存存在的意义是为了指令另一块内存单元,所以我们把它称作指针

  • 32 位程序中,这块内存大小是 4 个字节
  • 64 位程序中,这块内存大小是 8 个字节       
#include <stdio.h>

int main(int argc, const char* argv[]) {
	int* pi = NULL;			// int* 指针
	double* pd = NULL;		// double* 指针 

	printf("pi size = %d\n", sizeof(pi));	// 32位程序,pi size = 4; 64位程序,pi size = 8
	printf("pd size = %d\n", sizeof(pd));	// 32位程序,pd size = 4; 64位程序, pd size = 8

	return 0;
}

2.3 常量指针

        常量指针,本质是一个指针,用 const 修饰的指针是常量指针,也就是说这个指针指向另一块存储常量的内存单元,所以我们不能通过常量指针来修改它指向的另一块内存单元的值。

#include <stdio.h>

int main(int argc, const char* argv[]) {
	for (int i = 0; i < argc; i++) {
		// *argv[i] = '1';	// error: 常量指针,这个指针指向的内存单元的值是常量,不能被修改
		printf("argv[%d] = %s\n", i, argv[i]);
	}
		

	return 0;
}

        所以,main 函数的第二个参数声明为 const char* (常量字符指针) 可以防止我们在实际的开发中,不小心修改了 argv 数组里的元素指向的内存单元的值。

2.4 指针常量

        指针常量,本质是一个常量,用指针类型修饰的常量是指针常量,常量就必须声明的时候初始化,初始化后整个程序运行期间都不能再修改。

#include <stdio.h>

int main(int argc, const char* argv[]) {
	int a = 1;
	int* const pi = &a;
	int b = 2;
	// pi = &b;		// error: 常量不能被修改

	return 0;
}

3. 反汇编角度分析 Hello World 程序

3.1 栈

        在计算机科学中,栈是一种后进先出(LIFO,last in first out)的数据结构,往栈中写数据,叫做入栈(push),将栈顶数据从栈中弹出,叫做出栈(pop)。

3.2 函数用栈传递参数

        函数用栈传递参数的原理是由调用者通过 push 指令将需要传递给被调用者的参数压入栈中,被调用者从栈中取得参数。

3.3 函数调用栈

        C 语言,所有函数的调用,是通过栈来实现的,当函数被调用时,其返回地址、参数以及局部变量会被压入栈中。当函数返回时,这些信息会从栈中弹出,以恢复到函数被调用之前的状态。我们可以称维护所有函数调用形成的这个栈空间为函数调用栈。

        特别要注意的是,一个 C 程序的栈是往下生长的,即栈底在高地址,栈顶在低地址,入栈,栈顶地址会变小,出栈,栈顶地址会变大。

3.4 函数栈帧

        在 C 语言中,从一个函数的进入,到这个函数返回,形成的栈空间,我们称为函数栈帧。函数栈帧不是固定不变的,而是随着函数的功能,栈帧空间可能随时在变大或缩小。

3.5 相关寄存器

  • ebp: extended base pointer,扩展基址指针寄存器,一般用于当进入一个函数时,存放该函数的栈帧基址(栈帧的栈底)。
  • esp: extended stack pointer,扩展栈指针寄存器,存放函数栈的栈顶
  • ebp 和 esp 配合使用,共同协作维护着正在运行的函数的栈帧
  • ebx: extended base register,扩展基址寄存器,特别是在数组和字符串操作中,它可以用来存储数组或字符串的基地址。此外,在调用操作系统函数时,ebx 有时也用于传递参数。
  • esi: extended source index register,扩展源索引寄存器,在字符串操作中非常有用。它通常用于存储源字符串或数据数组的起始地址,在字符串指令(如 stos 等)中自动递增,以便按顺序处理数据。
  • edi: extended destination index register,扩展目的索引寄存器,与 esi 相对应,在字符串操作中用于存储目标字符串或数据数组的起始地址。与 esi 相似,edi 也在字符串指令中自动递增,以接收来自源地址的数据。
  • ecx: extended count register,扩展计数寄存器,在循环操作中尤其重要。它存储了循环的迭代次数,很多指令(如以 rep 为前缀的字符串操作指令)都会递减 ecx 的值来控制循环次数。
  • eax: extended accumulator register,eax 寄存器通常用作累加器,在算术和逻辑运算中扮演主要角色。它经常用于存储操作数、结果以及中间值。在函数调用中,eax 常用于存放函数的返回值。

3.6 相关汇编指令

  • mov: 数据传送指令,比如:mov eax, 3        ;将 3 这个立即数传送给 eax 寄存器
  • push: 往栈中压入数据,CUP操作原理:(1) esp - 存储压入栈数据占用的字节数。 (2) 修改栈顶数据为要压入栈中的数据。比如:push eax        ;将 eax 寄存器的数据压入栈中
  • pop: 从栈顶弹出数据, CUP操作原理:(1) 栈顶数据弹出栈  (2) esp + 从栈顶弹出的数据占用的字节数。比如:pop eax        ;将栈顶数据弹出,送到 eax 寄存器中
  • sub: 减法指令
  • add: 加法指令
  • rep: 在汇编语言中,rep 的作用是根据 ecx 的值,循环执行跟在其后的指令,直到 ecx = 0 时为止。
  • cmp: 根据操作结果,设置标志寄存器相关位的值,其他相关指令就可以拿到寄存器相关位的值进行相关操作
  • stos: 字符串操作指令,比如:stos dword ptr es:[edi],是将 eax 的值传送给 es:[edi] 为首地址的 4 个字节单元中
  • xor: 二进制异或操作,即 1 xor 0 = 1
  • call: 转到标号处执行指令
  • ret: 跟 call 配合,控制 cpu 返回到调用该函数的指令的下一条指令执行

3.7 汇编代码分析

int main(int argc, const char* argv[]) {
00E517E0  push        ebp       ;调用者的栈帧基址入栈
00E517E1  mov         ebp,esp   ;构建自己的栈帧基址
00E517E3  sub         esp,0C0h  ;栈顶往上移 0C0h 字节
00E517E9  push        ebx       ;调用者的 ebx 入栈,esp = esp-4
00E517EA  push        esi       ;调用者的 esi 入栈,esp = esp-4
00E517EB  push        edi       ;调用者的 edi 入栈,esp = esp-4
00E517EC  mov         edi,ebp   ;edi = ebp
00E517EE  xor         ecx,ecx   ;等价于 mov ecx, 0; 但 xor 指令更高效
00E517F0  mov         eax,0CCCCCCCCh  
00E517F5  rep stos    dword ptr es:[edi]    ;rep 的作用是根据 cx 的值,循环执行跟在其后的指令,直到 cx = 0 时为止。
                                            ;stos 串传送指令,相当于 mov es:[di], eax 
                                            ;df = 0, edi = edi + 4; df = 1, edi = edi - 4;
    printf("Hello World\n");
00E517F7  push        offset string "Hello World\n" (0E57B30h)  ;传递给 printf 函数的参数入栈,esp = esp-4
                                                                ;offset:取得字符串 "Hello World\n" 在内存的首地址
00E517FC  call        _printf (0E510CDh)    ;调用 printf 函数 
00E51801  add         esp,4     ;esp = esp+4,即销毁传递给 printf 函数的参数 "Hello World\n" 在内存的首地址占用的空间
    return 0;
00E51804  xor         eax,eax   ;函数返回值放在 eax 寄存器,该指令相当于 mov eax, 0; 但 xor 指令更高效
}
00E51806  pop         edi       ;还原调用者的 edi,esp = esp+4
00E51807  pop         esi       ;还原调用者的 esi,esp = esp+4
00E51808  pop         ebx       ;还原调用者的 ebx,esp = esp+4
00E51809  add         esp,0C0h  ;栈顶往下移 0C0h 字节
00E5180F  cmp         ebp,esp   ;计算 ebp-esp,然后根据结果对 cpu 的标志寄存器进行设置
                                ;目的是让下一条指令 call __RTC_CheckEsp,检测该函数栈帧建立前跟销毁该函数栈帧后的esp 是否一致
00E51811  call        __RTC_CheckEsp (0E5123Fh)  ;检测标志寄存器相关位的值,从而判断 ebp 跟 esp 是否相等
00E51816  mov         esp,ebp   ;恢复调用者的栈顶
00E51818  pop         ebp       ;恢复调用者的栈顶基址
00E51819  ret                   ;调用者通过 call 指令调用函数会 push eip, 自己必须 ret 指令,pop eip
3.7.1 invoke_main() 函数调用了 main 函数

        查看 VS2019 的调用堆栈窗口,我们可以知道,框架的 invoke_main() 函数调用了我们

main 函数。

3.7.2 main 函数的栈帧建立和销毁过程

(1)进入主函数栈的初始状态

(2)cpu 执行 push ebp

(3)cpu 执行 mov ebp, esp

(4)cpu 执行 sub esp, 0C0h

(5)cpu 执行 push ebx

(6)cpu 执行 push esi

(7)cpu 执行 push edi

(8)cpu 执行以下指令对栈没影响

00E517EC  mov         edi,ebp   ;edi = ebp
00E517EE  xor         ecx,ecx   ;等价于 mov ecx, 0; 但 xor 指令更高效
00E517F0  mov         eax,0CCCCCCCCh  
00E517F5  rep stos    dword ptr es:[edi]    ;rep 的作用是根据 cx 的值,循环执行跟在其后的指令,直到 cx = 0 时为止。
                                            ;stos 串传送指令,相当于 mov es:[di], eax 
                                            ;df = 0, edi = edi + 4; df = 1, edi = edi - 4;

(9)cpu 执行 push offset string "Hello World\n"

(10)cpu 执行 call _printf 指令,cpu 进入 _printf 函数执行完该函数包含的指令返回后,栈又回到了跟没执行 _printf 函数一样的状态。

(11)cup 执行 add esp, 4

(12)cpu 执行 xor eax, eax 对栈没影响

(13)cpu 执行 pop edi

        

(14)cpu 执行 pop esi

(15)cpu 执行 pop ebx

(16)cpu 执行 add esp, 0C0h

(17)cpu 执行以下指令对栈没影响

00E5180F  cmp         ebp,esp   ;计算 ebp-esp,然后根据结果对 cpu 的标志寄存器进行设置
                                ;目的是让下一条指令 call __RTC_CheckEsp,检测该函数栈帧建立前跟销毁该函数栈帧后的esp 是否一致
00E51811  call        __RTC_CheckEsp (0E5123Fh)  ;检测标志寄存器相关位的值,从而判断 ebp 跟 esp 是否相等

(18)cpu 执行 mov esp, ebp

(19)cpu 执行 pop ebp

此时,我们看到函数栈的状态回到了跟刚进入该函数时的初始状态是一致的。

3.7.3 "Hello World\n" 字符串在内存中是以 assii 码的形式保存

        

大写字母 H:assii 码为 72,十六进制表示为 0x48

小写字母 e:assii 码为 101,十六进制表示为 0x65

小写字母 l:assii 码为 108,十六进制表示为 0x6c

小写字母 o:assii 码为 111,十六进制表示为 0x6f

大写字母 W:assii 码为 87,十六进制表示为 0x57

小写字母 r:assii 码为 114,十六进制表示为 0x72

小写字母 d:assii 码为 100,十六进制表示为 0x64

换行符 \n:assii 码为 10,十六进制表示为 0x0a


http://www.kler.cn/news/283008.html

相关文章:

  • 浏览器缓存
  • 网络安全-安全渗透简介和安全渗透环境准备
  • 【CSP:202109-2】非零段划分(Java)
  • 4.sklearn-K近邻算法、模型选择与调优
  • MySQL集群技术1——编译部署mysql
  • “重启就能解决一切问题”,iPhone重启方法大揭秘
  • 解决:无法从域控制器读取配置信息
  • 2024.8.29 C++
  • C#面:ASP.NET MVC 中还有哪些注释属性用来验证?
  • RKNPU2从入门到实践 ---- 【8】借助 RKNN Toolkit lite2 在RK3588开发板上部署RKNN模型
  • 设计模式--装饰器模式
  • 理解torch.argmax() ,我是错误的
  • 融资和融券分别是什么意思,融资融券开通后能融到多少资金?
  • Datawhale X 李宏毅苹果书 AI夏令营_深度学习基础学习心得Task2.2
  • Java 入门指南:Java NIO —— Selector(选择器)
  • 【hot100篇-python刷题记录】【搜索二维矩阵】
  • 分布式锁的实现:ZooKeeper 的解决方案
  • hive数据迁移
  • 低代码革命:JNPF平台如何简化企业应用开发
  • Linux 中的中断响应机制
  • TCP keepalive和HTTP keepalive区别
  • SCP拷贝失败解决办法
  • 基于单片机的指纹识别考勤系统设计
  • Web应用服务器Tomcat
  • 基于STM32开发的智能家居温度控制系统
  • Linux下的使用字符设备驱动框架编写ADC驱动 ——MQ-4传感器
  • 我在高职教STM32——ADC电压采集与光敏电阻(2)
  • rnn-手动实现
  • 区块链入门
  • Element Plus上传图片前,对图片进行压缩