IT评测·应用市场-qidao123.com技术社区

标题: 由浅入深学习 C 语言:Hello World【提高篇】 [打印本页]

作者: 傲渊山岳    时间: 2024-8-31 16:37
标题: 由浅入深学习 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 程序代码

  1. #include <stdio.h>   
  2. int main(int argc, const char *argv[])
  3. {
  4.         for (int i = 0; i < argc; i++)    // 打印命令行参数
  5.                 printf("argv[%d] = %s\n", i, argv[i]);
  6.         printf("Hello World\n");       
  7.         return 0;        
  8. }
复制代码

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
   
  (3)argv 是函数的第二个参数,参数类型是 const char* 数组,也就是说这个参数是一个数组,数组的每一项存的数据类型是 const char* (常量字符指针),指向每一个命令行参数字符串的首地址。
比如,我们的 Hello World 程序,在 linux 平台下,生成 main.out 可执行文件。 
   
  

   
  

2.2 指针

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

  1. #include <stdio.h>
  2. int main(int argc, const char* argv[]) {
  3.         int* pi = NULL;                        // int* 指针
  4.         double* pd = NULL;                // double* 指针
  5.         printf("pi size = %d\n", sizeof(pi));        // 32位程序,pi size = 4; 64位程序,pi size = 8
  6.         printf("pd size = %d\n", sizeof(pd));        // 32位程序,pd size = 4; 64位程序, pd size = 8
  7.         return 0;
  8. }
复制代码


2.3 常量指针

        常量指针,本质是一个指针,用 const 修饰的指针是常量指针,也就是说这个指针指向另一块存储常量的内存单元,所以我们不能通过常量指针来修改它指向的另一块内存单元的值。
  1. #include <stdio.h>
  2. int main(int argc, const char* argv[]) {
  3.         for (int i = 0; i < argc; i++) {
  4.                 // *argv[i] = '1';        // error: 常量指针,这个指针指向的内存单元的值是常量,不能被修改
  5.                 printf("argv[%d] = %s\n", i, argv[i]);
  6.         }
  7.                
  8.         return 0;
  9. }
复制代码
        所以,main 函数的第二个参数声明为 const char* (常量字符指针) 可以防止我们在实际的开发中,不小心修改了 argv 数组里的元素指向的内存单元的值。
2.4 指针常量

        指针常量,本质是一个常量,用指针类型修饰的常量是指针常量,常量就必须声明的时候初始化,初始化后整个程序运行期间都不能再修改。
  1. #include <stdio.h>
  2. int main(int argc, const char* argv[]) {
  3.         int a = 1;
  4.         int* const pi = &a;
  5.         int b = 2;
  6.         // pi = &b;                // error: 常量不能被修改
  7.         return 0;
  8. }
复制代码
3. 反汇编角度分析 Hello World 程序

3.1 栈

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

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

        C 语言,全部函数的调用,是通过栈来实现的,当函数被调用时,其返回地址、参数以及局部变量会被压入栈中。当函数返回时,这些信息会从栈中弹出,以规复到函数被调用之前的状态。我们可以称维护全部函数调用形成的这个栈空间为函数调用栈。
        特殊要留意的是,一个 C 程序的栈是往下生长的,即栈底在高地址,栈顶在低地址,入栈,栈顶地址会变小,出栈,栈顶地址会变大。
3.4 函数栈帧

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


3.6 相关汇编指令


3.7 汇编代码分析

  1. int main(int argc, const char* argv[]) {
  2. 00E517E0  push        ebp       ;调用者的栈帧基址入栈
  3. 00E517E1  mov         ebp,esp   ;构建自己的栈帧基址
  4. 00E517E3  sub         esp,0C0h  ;栈顶往上移 0C0h 字节
  5. 00E517E9  push        ebx       ;调用者的 ebx 入栈,esp = esp-4
  6. 00E517EA  push        esi       ;调用者的 esi 入栈,esp = esp-4
  7. 00E517EB  push        edi       ;调用者的 edi 入栈,esp = esp-4
  8. 00E517EC  mov         edi,ebp   ;edi = ebp
  9. 00E517EE  xor         ecx,ecx   ;等价于 mov ecx, 0; 但 xor 指令更高效
  10. 00E517F0  mov         eax,0CCCCCCCCh  
  11. 00E517F5  rep stos    dword ptr es:[edi]    ;rep 的作用是根据 cx 的值,循环执行跟在其后的指令,直到 cx = 0 时为止。
  12.                                             ;stos 串传送指令,相当于 mov es:[di], eax
  13.                                             ;df = 0, edi = edi + 4; df = 1, edi = edi - 4;
  14.     printf("Hello World\n");
  15. 00E517F7  push        offset string "Hello World\n" (0E57B30h)  ;传递给 printf 函数的参数入栈,esp = esp-4
  16.                                                                 ;offset:取得字符串 "Hello World\n" 在内存的首地址
  17. 00E517FC  call        _printf (0E510CDh)    ;调用 printf 函数
  18. 00E51801  add         esp,4     ;esp = esp+4,即销毁传递给 printf 函数的参数 "Hello World\n" 在内存的首地址占用的空间
  19.     return 0;
  20. 00E51804  xor         eax,eax   ;函数返回值放在 eax 寄存器,该指令相当于 mov eax, 0; 但 xor 指令更高效
  21. }
  22. 00E51806  pop         edi       ;还原调用者的 edi,esp = esp+4
  23. 00E51807  pop         esi       ;还原调用者的 esi,esp = esp+4
  24. 00E51808  pop         ebx       ;还原调用者的 ebx,esp = esp+4
  25. 00E51809  add         esp,0C0h  ;栈顶往下移 0C0h 字节
  26. 00E5180F  cmp         ebp,esp   ;计算 ebp-esp,然后根据结果对 cpu 的标志寄存器进行设置
  27.                                 ;目的是让下一条指令 call __RTC_CheckEsp,检测该函数栈帧建立前跟销毁该函数栈帧后的esp 是否一致
  28. 00E51811  call        __RTC_CheckEsp (0E5123Fh)  ;检测标志寄存器相关位的值,从而判断 ebp 跟 esp 是否相等
  29. 00E51816  mov         esp,ebp   ;恢复调用者的栈顶
  30. 00E51818  pop         ebp       ;恢复调用者的栈顶基址
  31. 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 执行以下指令对栈没影响
  1. 00E517EC  mov         edi,ebp   ;edi = ebp
  2. 00E517EE  xor         ecx,ecx   ;等价于 mov ecx, 0; 但 xor 指令更高效
  3. 00E517F0  mov         eax,0CCCCCCCCh  
  4. 00E517F5  rep stos    dword ptr es:[edi]    ;rep 的作用是根据 cx 的值,循环执行跟在其后的指令,直到 cx = 0 时为止。
  5.                                             ;stos 串传送指令,相当于 mov es:[di], eax
  6.                                             ;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 执行以下指令对栈没影响
  1. 00E5180F  cmp         ebp,esp   ;计算 ebp-esp,然后根据结果对 cpu 的标志寄存器进行设置
  2.                                 ;目的是让下一条指令 call __RTC_CheckEsp,检测该函数栈帧建立前跟销毁该函数栈帧后的esp 是否一致
  3. 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
   

免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。




欢迎光临 IT评测·应用市场-qidao123.com技术社区 (https://dis.qidao123.com/) Powered by Discuz! X3.4