热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

重学C语言(一):熟悉汇编语言

本文是笔者的读书笔记。若有缘,欢迎批评指正补充。面向:略知程序编译的过程,但不是很清楚每一步的细节还不能熟练使用调试器想更深入了解编程语言和程序的背后发生了什么没事闲来看看技术文章

本文是笔者的读书笔记。若有缘,欢迎批评指正补充。

面向:

  • 略知程序编译的过程,但不是很清楚每一步的细节
  • 还不能熟练使用调试器
  • 想更深入了解编程语言和程序的背后发生了什么
  • 没事闲来看看技术文章的人

本文不包含:

  • 程序优化的方案
  • 程序的设计方式
  • 编写实用软件的技巧

基础知识:

  • 已经掌握了“常规”的C语言的编写技能
  • 知道一些“常见”的计算机名词和它们的含义
  • 通过查询文档(man ,Google)可以大致了解未知API的使用

代码

本文使用如下的代码(hello.c)。

 1 #include 
 2 
 3 int main(int argc, char *argv[]) {
 4 
 5     asm volatile("# printf begin");
 6 
 7     printf("Hello, world! %d %s\n", argc, argv[0]);
 8 
 9     asm volatile("# printf end");
10 
11     return 0;
12 }

为了方便阅读生成后的汇编代码,我们可以在想观察的函数调用前后加上特定的标识,这样只需要从生成后的汇编代码中查找这些字符串就能定位了。

另外,编译使用gcc,命令如下:

$ gcc hello.c -Wall -g -O0 -S -o hello.s
$ gcc hello.s -o hello.out
$ objdump -d hello.out > hello.asm

这里列出在笔者的环境下最后生成的 hello.asm 的 main 函数的部分。
当然也可以阅读 .s 文件,只看 CPU 执行的指令部分的话,两者的内容是一样的。

000000000000064a 
: 64a: 55 push %rbp 64b: 48 89 e5 mov %rsp,%rbp 64e: 48 83 ec 10 sub $0x10,%rsp 652: 89 7d fc mov %edi,-0x4(%rbp) 655: 48 89 75 f0 mov %rsi,-0x10(%rbp) 659: 48 8b 45 f0 mov -0x10(%rbp),%rax 65d: 48 8b 10 mov (%rax),%rdx 660: 8b 45 fc mov -0x4(%rbp),%eax 663: 89 c6 mov %eax,%esi 665: 48 8d 3d 98 00 00 00 lea 0x98(%rip),%rdi # 704 <_IO_stdin_used+0x4> 66c: b8 00 00 00 00 mov $0x0,%eax 671: e8 aa fe ff ff callq 520 676: b8 00 00 00 00 mov $0x0,%eax 67b: c9 leaveq 67c: c3 retq 67d: 0f 1f 00 nopl (%rax)

解析

官方文档

Intel 公开的 Intel 64 和 IA-32 架构手册可以在这个网站找到:
https://software.intel.com/en-us/articles/intel-sdm

官方网站同时还公布了扩展指令集的文档、性能优化文档等等。

寄存器

x86 架构中,以 e 开头的是 32 位寄存器,以 r 开头的是 64 位寄存器。
上面的汇编代码中出现的寄存器都属于 GPR(General-purpose Register),也就是说程序可以***操作这些寄存器的值,所以在使用方法上和内存并没有什么区别。

在上面的汇编命令中,用括号标起来的寄存器(比如 (%rax) ),代表这个寄存器的值所指向的内存地址里面保存着的值。
用C语言来说明的话就是 *(int *)rax 。
而如果在括号前面还有数值的话,就代表这个寄存器的值加减那个数值之后所指向的内存地址里面保存着的值。
比如 -0x10(%rbp) 的意思是,内存中 rbp 的值减 16(注意是 16 进制)的位置所储存的值。
同样用C语言来说明的话就是 *(int *)((char *)rbp - 0x10) 。

上面用C语言的解释可能不准确。
具体来说, mov 并没有指定数据的大小,也就是说具体是拷贝 32 位还是 64 位的数据取决于第一个参数,也就是数据源的寄存器的大小。
笔者分析后认为,652 处的 mov 会拷贝 32 位的数据,而 655 处的 mov 会拷贝 64 位的数据。

还有需要注意的一点是,比如 rax 和 eax 其实并不是两个独立的寄存器。 eax 所指的是 rax 的下面 32 位。
同时还有 ax , ah , al 。

重学C语言(一):熟悉汇编语言

编译器把 rsp (32 位的寄存器是 esp )专门用作了堆栈指针(SP,stack pointer)。
x86 的 SP 是向更小的方向伸展的,也就是说减小 rsp 的值就等于拿到了这部分的栈空间。

另外, rsp 必须对齐到 16 字节(即 rsp 的值必须是 16 的倍数)。
但是在 main 的里面并没有类似的命令。
笔者在 start 的部分找到了对齐的命令,汇编如下:

0000000000000540 <_start>:
 540:    31 ed                    xor    %ebp,%ebp
 542:    49 89 d1                 mov    %rdx,%r9
 545:    5e                       pop    %rsi
 546:    48 89 e2                 mov    %rsp,%rdx
 549:    48 83 e4 f0              and    $0xfffffffffffffff0,%rsp
 54d:    50                       push   %rax
 54e:    54                       push   %rsp
 54f:    4c 8d 05 9a 01 00 00     lea    0x19a(%rip),%r8        # 6f0 <__libc_csu_fini>
 556:    48 8d 0d 23 01 00 00     lea    0x123(%rip),%rcx        # 680 <__libc_csu_init>
 55d:    48 8d 3d e6 00 00 00     lea    0xe6(%rip),%rdi        # 64a 
564: ff 15 76 0a 20 00 callq *0x200a76(%rip) # 200fe0 <__libc_start_main@GLIBC_2.2.5> 56a: f4 hlt 56b: 0f 1f 44 00 00 nopl 0x0(%rax,%rax,1)

观察 564 处的命令可以猜出这段指令应该是在 main 之前被执行的。
而 549 处的命令应该就是对齐 rsp 的命令了(把 rsp 的最后4位变成0,也就是16的倍数了)。

函数调用

在 32 位架构中,函数调用时的参数都会放在栈空间当中(似乎也有放在寄存器里面的扩展)。
而在 64 位架构中,由于寄存器的数量变多了( r8 ~ r15 ),很多情况下可以直接把参数放在寄存器里。

原书中采用的是 32 位架构下的汇编代码,而笔者分析的是 64 位的汇编代码,因此函数调用的步骤有了不小的差异。
下面大多都是笔者个人的分析。

“把参数放在寄存器里”这个条件对于主函数应该也同样适用。
主函数的参数(有两个)应该分别储存在了 edi 和 rsi 当中。第一个是 int 类型,在这里是 32 位的,所以被放在了 edi 里面。而指针的数组(就是指针)在 64 位架构下肯定是 64 位的,所以被储存在了 rsi 当中。

而在下面的步骤里,由于要调用 printf 函数,所以需要先把 edi 和 rsi 的值保存到栈中。这样才能修改这些寄存器的值。所以程序先将 SP 的值减小了 16,扩充了 16 字节的栈空间。
接下来,程序就把两个参数都储存到了栈里面。

而再接下来的一连串命令,先是储存了 argv[0] 到了 rdx 当中,又把 argc 储存到了 esi 当中,最后又把格式化用的字符串(的地址)储存到了 rdi 当中。
这个格式化用的字符串是写在执行程序里面的,可以从取地址的时候用到了 rip 寄存器(用来存储 PC 的 64 位寄存器)这一点来推断。这也是为什么这种字符串在执行时是不能被改变的。

接下来就是函数调用的命令。而后返回。

 nop 是什么都不做的意思(no-operation)。这里填入 nop 是为了让后面的函数对齐到 16 字节处(67d 向后数 3 个刚好是 680,也就是对齐到了 16 字节)。

笔者认为,破坏了 edi 和 rsi 却没有从栈里面复原是因为后面没有用到这些值。对此猜测并没有做更深一步的检验。

总结

在这一部分,笔者学习了寄存器的基本用途,函数调用的模式。
经过这一天的洗礼,笔者现在可以以 CPU 大约五十亿分之一的速度来理解程序了,耶。


推荐阅读
  • VScode格式化文档换行或不换行的设置方法
    本文介绍了在VScode中设置格式化文档换行或不换行的方法,包括使用插件和修改settings.json文件的内容。详细步骤为:找到settings.json文件,将其中的代码替换为指定的代码。 ... [详细]
  • 本文介绍了在开发Android新闻App时,搭建本地服务器的步骤。通过使用XAMPP软件,可以一键式搭建起开发环境,包括Apache、MySQL、PHP、PERL。在本地服务器上新建数据库和表,并设置相应的属性。最后,给出了创建new表的SQL语句。这个教程适合初学者参考。 ... [详细]
  • 向QTextEdit拖放文件的方法及实现步骤
    本文介绍了在使用QTextEdit时如何实现拖放文件的功能,包括相关的方法和实现步骤。通过重写dragEnterEvent和dropEvent函数,并结合QMimeData和QUrl等类,可以轻松实现向QTextEdit拖放文件的功能。详细的代码实现和说明可以参考本文提供的示例代码。 ... [详细]
  • XML介绍与使用的概述及标签规则
    本文介绍了XML的基本概念和用途,包括XML的可扩展性和标签的自定义特性。同时还详细解释了XML标签的规则,包括标签的尖括号和合法标识符的组成,标签必须成对出现的原则以及特殊标签的使用方法。通过本文的阅读,读者可以对XML的基本知识有一个全面的了解。 ... [详细]
  • 《数据结构》学习笔记3——串匹配算法性能评估
    本文主要讨论串匹配算法的性能评估,包括模式匹配、字符种类数量、算法复杂度等内容。通过借助C++中的头文件和库,可以实现对串的匹配操作。其中蛮力算法的复杂度为O(m*n),通过随机取出长度为m的子串作为模式P,在文本T中进行匹配,统计平均复杂度。对于成功和失败的匹配分别进行测试,分析其平均复杂度。详情请参考相关学习资源。 ... [详细]
  • 本文介绍了数据库的存储结构及其重要性,强调了关系数据库范例中将逻辑存储与物理存储分开的必要性。通过逻辑结构和物理结构的分离,可以实现对物理存储的重新组织和数据库的迁移,而应用程序不会察觉到任何更改。文章还展示了Oracle数据库的逻辑结构和物理结构,并介绍了表空间的概念和作用。 ... [详细]
  • 本文介绍了九度OnlineJudge中的1002题目“Grading”的解决方法。该题目要求设计一个公平的评分过程,将每个考题分配给3个独立的专家,如果他们的评分不一致,则需要请一位裁判做出最终决定。文章详细描述了评分规则,并给出了解决该问题的程序。 ... [详细]
  • 本文介绍了C++中省略号类型和参数个数不确定函数参数的使用方法,并提供了一个范例。通过宏定义的方式,可以方便地处理不定参数的情况。文章中给出了具体的代码实现,并对代码进行了解释和说明。这对于需要处理不定参数的情况的程序员来说,是一个很有用的参考资料。 ... [详细]
  • 本文主要解析了Open judge C16H问题中涉及到的Magical Balls的快速幂和逆元算法,并给出了问题的解析和解决方法。详细介绍了问题的背景和规则,并给出了相应的算法解析和实现步骤。通过本文的解析,读者可以更好地理解和解决Open judge C16H问题中的Magical Balls部分。 ... [详细]
  • 本文介绍了一种划分和计数油田地块的方法。根据给定的条件,通过遍历和DFS算法,将符合条件的地块标记为不符合条件的地块,并进行计数。同时,还介绍了如何判断点是否在给定范围内的方法。 ... [详细]
  • 本文介绍了解决二叉树层序创建问题的方法。通过使用队列结构体和二叉树结构体,实现了入队和出队操作,并提供了判断队列是否为空的函数。详细介绍了解决该问题的步骤和流程。 ... [详细]
  • 本文介绍了C函数ispunct()的用法及示例代码。ispunct()函数用于检查传递的字符是否是标点符号,如果是标点符号则返回非零值,否则返回零。示例代码演示了如何使用ispunct()函数来判断字符是否为标点符号。 ... [详细]
  • 动态规划算法的基本步骤及最长递增子序列问题详解
    本文详细介绍了动态规划算法的基本步骤,包括划分阶段、选择状态、决策和状态转移方程,并以最长递增子序列问题为例进行了详细解析。动态规划算法的有效性依赖于问题本身所具有的最优子结构性质和子问题重叠性质。通过将子问题的解保存在一个表中,在以后尽可能多地利用这些子问题的解,从而提高算法的效率。 ... [详细]
  • CF:3D City Model(小思维)问题解析和代码实现
    本文通过解析CF:3D City Model问题,介绍了问题的背景和要求,并给出了相应的代码实现。该问题涉及到在一个矩形的网格上建造城市的情景,每个网格单元可以作为建筑的基础,建筑由多个立方体叠加而成。文章详细讲解了问题的解决思路,并给出了相应的代码实现供读者参考。 ... [详细]
  • 高质量SQL书写的30条建议
    本文提供了30条关于优化SQL的建议,包括避免使用select *,使用具体字段,以及使用limit 1等。这些建议是基于实际开发经验总结出来的,旨在帮助读者优化SQL查询。 ... [详细]
author-avatar
爱看好电影110_275
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有