Linux C :C的汇编码生成
想知道一段C语言写的代码对应生成的汇编语言代码是什么?那么需要了解:
1)一些基本的编译过程原理
2)常用的寄存器有哪些,专门来做哪些事
3)分析C语言代码对应的堆栈情况
1)一些基本的编译过程原理
C的汇编代码是一个或多个cpp文件通过编译器处理而成的,而一个编译器通常要通过词法分析,语法分析,语义分析才能够生成汇编代码。以gcc为例,一个cpp文件同通过编译器生成汇编代码(*.s)文件,再通过汇编器生成出机器能够识别的指令代码(*.o)文件,最后同通过链接器,将多个指令文件合成一个大指令文件(*.out) 供机器去执行。
C的汇编码生成是一个复杂的算法。但是,我们可以同通过执行过程中的堆栈情况和寄存器使用情况来反推出汇编码是什么。汇编码也可以模拟出堆栈情况和寄存器使用情况来推测出C的代码是什么,当然这个反汇编过程可以保留好程序逻辑,数据结构,但是保留不住源代码的变量、引用名称。
一个 .out 的文件执行映像如下图,符号 “_brk”表示bss段的结束,机器加载文件通常从文件头开始加载,加载到bss段 _brk标志结束。
栈区:栈区是向低地址扩展的,是一块连续的内存的区域。栈顶的地址和栈的最大容量是操作系统给程序预先规定好的,大小在进程分配时是确定的。
堆区:堆区是向高地址扩展的,是不连续的内存区域(这是由于系统是用链表来存储的空闲内存地址的,自然是不连续的是动态分配的),因为会手动分配内存通常会预留大一些,大小不固定。
由于栈区是向低地址扩展,当int数据类型第一个压栈时其地址表示为 -4(%ebp) ,再压一个8字节double类型,其地址表示为 -12(%ebp). ,其中ebp 表示 ebp寄存器(扩展基址指针寄存器)。
再Linux 中写好 .c文件并编译出 汇编文件的命令例子 gcc -O0 -S test.c -o test.s ,其中-O0代表不去优化(缺省默认)
在smtest.c中写入
#include <stdlib.h>
#include <stdio.h>
void main(){int a,b,c;a=2; b=3;c=4;c= c+a*3;printf("%d",c);
}
编译出来的 test.s 的汇编码为
.file "smtest.c".text.section .rodata
.LC0:.string "%d".text.globl main.type main, @function
main:
.LFB6:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl $2, -12(%rbp)movl $3, -8(%rbp)movl $4, -4(%rbp)movl -12(%rbp), %edxmovl %edx, %eaxaddl %eax, %eaxaddl %edx, %eaxaddl %eax, -4(%rbp)movl -4(%rbp), %eaxmovl %eax, %esileaq .LC0(%rip), %rdimovl $0, %eaxcall printf@PLTnopleave.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE6:.size main, .-main.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0".section .note.GNU-stack,"",@progbits.section .note.gnu.property,"a".align 8.long 1f - 0f.long 4f - 1f.long 5
0:.string "GNU"
1:.align 8.long 0xc0000002.long 3f - 2f
2:.long 0x3
3:.align 8
4:
我们最主要关注的是main代码段的汇编码
2)常用的寄存器
下表展示的是16bit寄存器 , 32bit寄存器的前缀是 e, 64bit的寄存器前缀是 r。例如 bp/ebp/rbp
CFI寄存器编号 | 16bit寄存器名称 | 常用用途 | ||
通用寄存器 | 1 | AX=(AH,AL) | 累加器 | 常用于乘、除法和函数返回值 |
2 | BX=(BH,BL) | 基址地址寄存器 | 常用于内存数据的地址 | |
3 | CX=(CH,CL) | 计数寄存器 | 常用于循环指令的循环计数 | |
4 | DX=(DH,DL) | 数据寄存器 | 常用在字乘法和除法指令中,作辅助累加器(即存放乘积或被除数的高16位)和在输入输出指令中存放16位的端口地址 | |
5 | SP | 堆栈顶指针寄存器 | 其内存放着一个指针,该指针永远指向系统栈最上面一个栈帧的栈顶。通过PUSH和POP指令控制指针移动 | |
6 | BP/FP | 堆栈基址寄存器 | 其内存放着一个指针,该指针永远指向当前函数的栈帧的底部地址。 | |
7 | SI | 源变址寄存器 | 在串处理指令中,SI用作隐含的源串地址 | |
8 | DI | 目的变址寄存器 | 在串处理指令中,DI用做隐含的目的串地址 | |
专用寄存器 | IP/PC | 指令寄存器 | 保存CPU即将执行的一条指令的偏移地址 | |
CS | 代码段寄存器 | 用来存放内存代码段区域的入口地址 | ||
DS | 数据段寄存器 | 用来存放内存数据段区域的入口地址 | ||
SS | 堆栈段寄存器 | 用来存放内存堆栈段区域的入口地址 | ||
ES | 附加数据段寄存器 | 常在串处理当作DS寄存器来备用 | ||
FS | FS辅助段寄存器 | 作为段寄存器备用,常被操作系统用于指向当前活动线程的TEB结构(线程结构) | ||
GS | GS辅助段寄存器 | 作为段寄存器备用,在Windows中,该GS寄存器用于管理线程特定的内存。linux内核用于GS访问cpu特定的内存 |
所谓的栈帧,就是一段代码块所对应的栈区域。同栈帧下的变量,对象的生命周期都是一样的。把栈比作一栋楼,则栈帧表示连续好几层楼。栈是由栈帧构成的,越靠近栈顶的栈帧,其栈帧内 变量的生命周期越短,内存越早释放。栈帧的起始地址通常由BP寄存器保存,在X86 CPU 通常由FP寄存器保存。
3)分析C语言代码对应的堆栈情况
CFI全称是Call Frame Instrctions, 即调用框架指令。CFI提供的调用框架信息, 为实现堆栈回绕(stack unwiding)或异常处理(exception handling)提供了方便, 它在汇编指令中插入指令符(directive), 以生成DWARF可用的堆栈回绕信息。CFI调用栈帧信息,编译器用于描述函数中发生的事情的方式。CFA调用栈帧地址,表示调用函数的时的堆栈指针位置,在前一个调用框架中调用当前函数时的栈顶指针。例如A方法调用了B方法,之后调用了C方法,B和C方法都调用了D方法,结果在执行过程中,D方法抛出了异常。那么怎么样才可以知道D方法是B抛出的还是C抛出的呢?这个需要一个记录,主要用于记录栈帧的起始地址。
可以把CFA 看成是一个数据结构 ,它的成员包含了一个地址,还有一个对标的寄存器
指令符的意义链接如下:
https://sourceware.org/binutils/docs/as/CFI-directives.html#CFI-directives
列出几个:
.cfi_startproc 表示每个函数的开头标志。它初始化一些内部数据结构。对应的用.cfi. endproc 来表示关闭函数的标志。
.cfi_def_cfa_offset 16 距离栈帧的距离16,在此偏移后的地址用CFA的基址寄存器保存,程序刚开始的默认基址寄存器是 5号SP寄存器
.cfi_offset [6,-16] 把6号寄存器BP 的值保存在CFA - 16 处
.cfi_def_cfa_register 6 CFA的基址寄存器改用6号寄存器保存,同时原先寄存器的值也挪到6号,
在每个C函数、代码块的入口处,编译后的代码会完成如下功能:
- 将CPU上的IP指令寄存器中的值压栈
- 让IP指向保存的地址建立栈帧
- 向低地址处移动SP为局部变量和临时变量分配存储空间
C代码: int a,b,c; 对应的汇编码
pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rsp
对应步骤内容:,其中xxxx表示执行main函数前的堆栈内容
0)进入main函数后,首先栈中被压栈的第一个元素是 IP寄存器的内容,main方法的指令地址。之后压栈rbp,此时的rbp是上一个栈帧地址 ,CFA 地址也是上一个栈帧的位置 。压栈完后,SP指针在上图的BP处。
1)略:在CFI框架中,CFA指向的寄存器不变。但CFA地址变更为 CFA指向寄存器位置偏移16个字节。意味者CFA地址位于上图BP处还往高地址16个字节的 位置。
2)略:在CFI框架中,将6号寄存器rbp的值保存在 CFI框架的CFA - 16 对应的位置。 即 上图BP位置。
3)把栈顶地址赋值给BP寄存器,建立栈帧。 此时的BP寄存器存的是上图BP位置的栈地址,而上图栈中的BP存的是上一个栈帧的栈地址。
4)略:在CFI框架中,把CFA指向的寄存器改为6号寄存器 rbp
5)将rsp往低地址偏移16个字节,预分配16个字节的内存空间
movl $2, -12(%rbp) # a = 2movl $3, -8(%rbp) # b = 3movl $4, -4(%rbp) # c = 4 movl -12(%rbp), %edx # temp1 = a movl %edx, %eax # temp2 = temp1addl %eax, %eax # temp2 = temp2 + temp2 =4addl %edx, %eax # temp2 = temp2 + temp1 =6addl %eax, -4(%rbp) # c = c +temp2 =10
因为大部分的程序,都加了优化编译选项。在栈的使用方面都作出了些许变化。例如,x86-64引入了一个新的特性, 可以使用栈顶之外128字节的地址,即不用直接先分配空间,而是先使用空间再在适当的时机分配。x86-64遵循ABI规则。
带函数跳转的汇编码
#include <stdlib.h>
#include <stdio.h>
int fun2(int fa2){return fa2 *10;
}
int fun(int fa1, int fb1){int cc=40;int ret = fun2(fa1)+fb1*cc;return ret;
}
void main(){int a,b,c;a=2; b=3;c=4;c= fun(a,b);printf("%d",c);
}
生成处来的汇编文件如下图
.file "smtest.c".text.globl fun2.type fun2, @function
fun2:
.LFB6:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6movl %edi, -4(%rbp)movl -4(%rbp), %edxmovl %edx, %eaxsall $2, %eaxaddl %edx, %eaxaddl %eax, %eaxpopq %rbp.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE6:.size fun2, .-fun2.globl fun.type fun, @function
fun:
.LFB7:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $24, %rspmovl %edi, -20(%rbp)movl %esi, -24(%rbp)movl $40, -8(%rbp)movl -20(%rbp), %eaxmovl %eax, %edicall fun2movl -24(%rbp), %edximull -8(%rbp), %edxaddl %edx, %eaxmovl %eax, -4(%rbp)movl -4(%rbp), %eaxleave.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE7:.size fun, .-fun.section .rodata
.LC0:.string "%d".text.globl main.type main, @function
main:
.LFB8:.cfi_startprocendbr64pushq %rbp.cfi_def_cfa_offset 16.cfi_offset 6, -16movq %rsp, %rbp.cfi_def_cfa_register 6subq $16, %rspmovl $2, -12(%rbp)movl $3, -8(%rbp)movl $4, -4(%rbp)movl -8(%rbp), %edxmovl -12(%rbp), %eaxmovl %edx, %esimovl %eax, %edicall funmovl %eax, -4(%rbp)movl -4(%rbp), %eaxmovl %eax, %esileaq .LC0(%rip), %rdimovl $0, %eaxcall printf@PLTnopleave.cfi_def_cfa 7, 8ret.cfi_endproc
.LFE8:.size main, .-main.ident "GCC: (Ubuntu 9.3.0-17ubuntu1~20.04) 9.3.0".section .note.GNU-stack,"",@progbits.section .note.gnu.property,"a".align 8.long 1f - 0f.long 4f - 1f.long 5
0:.string "GNU"
1:.align 8.long 0xc0000002.long 3f - 2f
2:.long 0x3
3:.align 8
4:
入口在main函数:在传参的过程中(a,b)-> (fa1,fb1) 的过程中,越右侧的参数越先入栈
#....fun:.......pushq %rbpmovq %rsp, %rbpsubq $24, %rspmovl %edi, -20(%rbp)movl %esi, -24(%rbp)...# main ...movl -8(%rbp), %edxmovl -12(%rbp), %eaxmovl %edx, %esimovl %eax, %edicall fun...
每个函数入口都会 pushq %rbp 用来保存栈帧,但是并不是每个函数都会预先分配内存 ,例如 fun2 中就没有 类似 subq $24, %rsp 。由于fun2是程序对应的最后一个栈帧,且访问栈外地址不超过128个字节。这样退栈就可以不需要做多余的操作。反正最后一个栈帧的栈顶地址也没啥用。
当执行到fun2 结束前:其栈内情况大致如下图所示
注:leave指令将ebp的值赋给esp,将栈顶元素退给ebp寄存器 ,等价于:
movl %ebp %esp
popl %ebp
RET指令则是将栈顶的返回地址弹出到IP寄存器然后按照EIP此时指示的指令地址继续执行程序。
所以在 fun2 完成 popq %rbp 和 ret 指令时, rbp 寄存器存的时②的地址,上图标记的中间的BP处 , rsp在fb1的地址处。
之后 fun1 完成 leave 和 ret 时 , rbp 寄存器存的时①的地址,上图标记的第一个的BP处 , rsp在a的地址处。
最后执行完main的 leave 和 ret。
Linux C :C的汇编码生成相关推荐
- 右值引用调用-汇编码分析
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 对于C++语言来说,右值引用是在C++11时引入的一个重要的功能,并在stl库中提供了右值引用的函数,便 ...
- 汇编码和机器码相互转化
由于要在PE文件中修改代码,本质上是在改十六进制的机器码(shellcode为经过处理的机器码),而我们阅读的是汇编码,汇编码和机器码相互转化可以更好的修改程序.本来想用pwn tool或OllyDb ...
- 虚函数继承与虚函数表-汇编码分析
(Owed by: 春夜喜雨 http://blog.csdn.net/chunyexiyu) 参考:https://www.equestionanswers.com/cpp/vptr-and-vta ...
- 如何优雅的查看 Java 代码的汇编码
转载请注明原创出处,谢谢! HappyFeet的博客 汇编码这种东西还是在上编译原理这门课的时候接触的比较多,工作之后几乎就没接触过了. 最近一次接触汇编码是阅读<深入理解 Java 虚拟机&g ...
- Go 学习笔记(20)— Go 操作 json 文件(编码生成 json、解码 json 为 map、解码 json 为 struct)
1. Json 概述 Go 语言对于标准格式的编码和解码都有良好的支持,由标准库中的 encoding/json . encoding/xml . encoding/asn1 等包提供支持并且这类包都 ...
- linux上的定时器上的jiffies,linux定时器和Jiffies汇.doc
linux定时器和Jiffies汇 1.linux HZ Linux核心几个重要跟时间有关的名词或变数,将介绍HZ.tick与jiffies. HZ Linux核心每隔固定周期会发出timer int ...
- linux下生成源程序控制流图,Linux下控制(统计)文件的生成的C代码实现
本文分享了Linux下控制(统计)文件的生成的C代码实现案例,供大家参考,具体内容如下 一.需求描述不定时地在Linux机器下的某目录中放入文件,文件内容中包含了用户号码.起止时间等字段,现要求编写一 ...
- linux内核定时器死机,浅析linux内核中timer定时器的生成和sofirq软中断调用流程
浅析linux内核中timer定时器的生成和sofirq软中断调用流程 mod_timer添加的定时器timer在内核的软中断中发生调用,__run_timers会spin_lock_irq(& ...
- linux下配置mysql默认编码utf8
linux下配置mysql默认编码utf8 下面是需要在对应地方加入的配置 [client] default-character-set=utf8[mysqld] character-set-serv ...
最新文章
- 看懂SQL Server的查询计划(绝对好文!)
- 2020年全球程序员收入报告出炉,字节跳动成唯一上榜中国公司
- 凡客即便走小米模式也很难
- python程序练习题第三章_python核心编程-第三章-习题
- Centos7 CMake升级
- c语言putchar_C语言实现变色的心!连机器都会变心,呵,男人!
- printstream_Java PrintStream clearError()方法与示例
- 一维FDTD等离子体的Matlab,修正过的一维FDTD等离子体MATLAB代码(公式修正)
- ElasticSearch预警服务-Watcher详解-Schedule配置
- 命令行方式添加打印机是比较简单的,现在我的问题是这样的,
- 无法定位程序输入点_Z21qRegisterResourceDataiPKhs0于动态链接库***.exe上
- Java 窗口设置图标及背景图片
- Charles 4.2.7 for Mac 中文破解版
- 行情 api php,股票实时数据接口说明,股票实时行情api接口
- PMP项目管理认证是什么?
- 医学统计学计算机操作教程第3版pdf,医学统计学 八年制 第3版pdf,9787117205047
- 网狐棋牌服务器IP地址配置方法
- Java之详解坦克大战游戏(六)
- 一段失传已久的相声!
- 笔记本摄像头正常却无法使用,提示未能创建视频预览,谁有解决办法?
热门文章
- EntityFramework Core 2.0自定义标量函数两种方式
- linux 本地账号密码无法登陆(shell可以登录),一直返回 登陆的login界面
- java java.lang.Long详解之三 大显神通的位移运算
- MATLAB 人脸定位
- C#关于事件的几个好例子
- ytu 2335: 0-1背包问题
- JDBC(Java Data Base Connectivity,java数据库连接)
- asp.net页面出错时的处理方法
- 为何出现Error Loading Midas.dll消息?
- 结构和类中字段的初始化以及用new来操作他们的构造函数