本文以printf为例,详细解析一个简单的printf调用里头,系统究竟做了什么,各寄存器究竟如何变化。

环境:

linux + gnu as assembler + ld linker

如何在汇编调用glibc的函数?其实也很简单,根据c convention call的规则,参数反向压栈,call,然后结果保存在eax里头。注意,保存的是地址。

在汇编里头,一切皆地址。(别纠结这个,别告诉我还有立即数……主要是要有一切皆地址的思想)

例如这个printf,在C里头,我们用得很多

int printf(const char *format, ...) 这里值得一提的是这个“...”是不定参数,也就是说后面有多少个参数,函数定义里头没有规定,感兴趣的可以google一下va_list相关的知识,这里就不展开了。

但是汇编怎么知道处理这个的呢?这里给个简单的解释,感兴趣的可以google一下“c convention call”了解更详细跟专业的解释。

例如当我们调用 result = printf( "%d %d", 12, a )的时候,编译器默认是这样处理的(除非函数定义声明了pascal call)。

在栈里头,先一次push a的地址,还有12这个立即数,再push "%d %d"这个字符串的地址,内存模型如下,x86的esp是往下增长的。

(这里是buttom,往下增长的是top)

&a

12

address of "%d %d"

-------------------------------------------(esp 指着这里 ,我们假设地址是4字节,12这个数也是4字节)

当call printf的时候,首先,push当前的eip入esp,解析esp+4所指的"%d %d",因为%d这样的特定字符都定义了后面每个参数的大小,所以只要解析“%d %d”,我们就可以知道栈里头参数的情况,例如esp+4+4就是一个int,esp+4+4+4是另外一个int。

当返回的时候,先pop到eip,也就是把eip还原到call之后马上要执行的机器码,这时,esp就指着“%d %d”,esp+4指着12,esp+8指着a的地址。esp里头的内容怎么处理,看需要吧,你也可以pop出来,也可以不pop。但为了效率着想,如果空间够用,通常不pop,直接用mov指令把下一次要用的参数move进去。返回指储存在eax里头。

这也一定程度上解释了为什么c convention call是反向压栈,这样编译器处理起来方便,特别对于这些va_list,因为va_list后面不能继续跟参数,va_list一定出现在函数的末尾,如果是对printf这类的函数使用pascal call,也就是参数正向压栈,汇编级别处理起来就特别麻烦了。

眼见为实,下面就用汇编写一个调用printf的,并用gdb跟踪寄存器,看看是否是上述的一样。

文件:test_printf.s

  1. .section .data
  2. format: .asciz "%d\n"
  3. .section .text
  4. .global _start
  5. _start:
  6. pushl $12
  7. pushl $format
  8. call printf
  9. movl $0, (%esp)
  10. call exit

使用如下命令编译,链接

$ as -g test_printf.s -o test_printf.o

$ ld -lc -I /lib/ld-linux.so.2 test_printf.o -o test_printf

as加入-g是要加入调试信息,ld的-lc是链接libc.a,-I是--dynamic-linker,/lib/ld-linux.so.2这个要看各人系统情况。链接libc跟ld库之后,生成test_printf

执行

$ ./test_printf

12

输出12,正常退出。

先用objdump看看test_printf里头的.text section

$ objdump -d test_printf

  1. Disassembly of section .text:
  2. 080481c0 <_start>:
  3. 80481c0:   6a 0c                   push   $0xc
  4. 80481c2:   68 cc 92 04 08          push   $0x80492cc
  5. 80481c7:   e8 d4 ff ff ff          call   80481a0 <printf@plt>
  6. 80481cc:   c7 04 24 00 00 00 00    movl   $0x0,(%esp)
  7. 80481d3:   e8 d8 ff ff ff          call   80481b0 <exit@plt>

下面使用gdb跟踪一下,看看上述是否正确。

$ gdb test_printf

(gdb) b _start  //设置断点到_start,主函数入口
Breakpoint 1 at 0x80481c0: file test_printf.s, line 7.
(gdb) run  //执行,遇到断点,停下,eip指着第7行,也就是第一条要执行的push指令
Starting program: /home/fengzh/research/c_and_asm/printf/test_printf

Breakpoint 1, _start () at test_printf.s:7
warning: Source file is more recent than executable.

7 pushl $12

(gdb) info reg   //察看寄存器状况,这里只显示需要注意的寄存器
esp            0xbffff870 0xbffff870
eip            0x80481c0 0x80481c0 <_start>  //指着第一条指令地址

(gdb) s  //执行一步,eip指着下一条指令地址
8 pushl $format
(gdb) info reg
esp            0xbffff86c 0xbffff86c  // 86c = 870 - 4,对比上一条的esp,小了4,也就是stack增长了4个字节
eip            0x80481c2 0x80481c2 <_start+2>

(gdb) s //执行一步,下一条就是printf系统调用
9 call printf
(gdb) info reg
esp            0xbffff868 0xbffff868  // 868 = 86c - 4,增长了4个字节
eip            0x80481c7 0x80481c7 <_start+7>

//////////重点来了

(gdb) s
0xb7e91110 in printf () from /lib/libc.so.6  //执行一步,正式进入printf
(gdb) info reg
esp            0xbffff864 0xbffff864  // 864 = 868 - c,新push进去4个字节
eip            0xb7e91110 0xb7e91110 <printf>

(gdb) x /1x $esp
0xbffff864: 0x080481cc  // esp的栈顶保存的是下一条要执行的代码的位置,movl的位置,(参考上面objdump的结果)

(gdb) s  //执行一步,printf已经执行完毕,
Single stepping until exit from function printf,
which has no line number information.
12   //这个是printf的输出
_start () at test_printf.s:10
10 movl $0, (%esp)
(gdb) info reg
eax            0x3                 3   // eax保存着这次printf的返回值,也就是被打印的字符数量,12\n,一共3个字符。
esp            0xbffff868 0xbffff868  // esp恢复到call printf之前的状态
eip            0x80481cc 0x80481cc <_start+12>   //恢复eip

(gdb) s  //执行movl指令,下一条是call exit
11 call exit
eax            0x3 3
esp            0xbffff868 0xbffff868
eip            0x80481d3 0x80481d3 <_start+19>

(gdb) x /1x $esp   
0xbffff868: 0x00000000   //esp并没有增长,因为printf之前的数据已经没用了,我没有把他们pop出来,而是直接用新的数据刷写esp所指的内存

(gdb) s
0xb7e77c80 in exit () from /lib/libc.so.6
(gdb) s
Single stepping until exit from function exit,
which has no line number information.
[Inferior 1 (process 1609) exited normally]

正常退出。一切都如上述。

经过这个简单的printf,我们可以清楚知道在一个glibc调用里头,汇编层面究竟是怎么做的,具体都做了些什么。

有了这个基础,如果各位想开发一门新语言,需要处理multiple return value的情况,就知道怎么做了。

例如,我需要处理这个函数[ a, b ] = function()

这个函数需要返回a跟b两个值。在c语言里头,构造一个struct,或者构造一个array,都是可行的。但是代码上看着就比较恶心,处理起来也麻烦。c语言返回值就只有一个,所以用一个eax就足够了,要么一个int,要么一个double,要么就一个地址,无论哪种情况,就1个寄存器就足够了(浮点型使用专门的st寄存器)

而如果是新的编译器需要处理这中语言,怎么做呢?在push参数之前,先push return value的address进去esp

例如

push a

push b

push parameter

在转跳函数里头,计算出参数a跟b的地址,之后把返回之存储到a跟b里头。就可以了。或者用eax,ebx之类的构造一个stack(这个我不大清楚是否可以,不过按照esp的思路,逻辑上应该是行得通的。)

希望对大家有用。

(转)详解汇编系统调用过程(以printf为例)的更多相关文章

  1. TOMCAT原理详解及请求过程(转载)

    转自https://www.cnblogs.com/hggen/p/6264475.html TOMCAT原理详解及请求过程 Tomcat: Tomcat是一个JSP/Servlet容器.其作为Ser ...

  2. 轻松学习Linux之详解系统引导过程

    轻松学习Linux之详解系统引导过程-1 轻松学习Linux之详解系统引导过程-2 本文出自 "李晨光原创技术博客" 博客,谢绝转载!

  3. Windows系统Git安装教程(详解Git安装过程)

    Windows系统Git安装教程(详解Git安装过程)   今天更换电脑系统,需要重新安装Git,正好做个记录,希望对第一次使用的博友能有所帮助! 获取Git安装程序   到Git官网下载,网站地址: ...

  4. ASP.NET 运行时详解 揭开请求过程神秘面纱

    对于ASP.NET开发,排在前五的话题离不开请求生命周期.像什么Cache.身份认证.Role管理.Routing映射,微软到底在请求过程中干了哪些隐秘的事,现在是时候揭晓了.抛开乌云见晴天,接下来就 ...

  5. 深度学习基础(CNN详解以及训练过程1)

    深度学习是一个框架,包含多个重要算法: Convolutional Neural Networks(CNN)卷积神经网络 AutoEncoder自动编码器 Sparse Coding稀疏编码 Rest ...

  6. TOMCAT原理详解及请求过程

    Tomcat: Tomcat是一个JSP/Servlet容器.其作为Servlet容器,有三种工作模式:独立的Servlet容器.进程内的Servlet容器和进程外的Servlet容器. Tomcat ...

  7. Tomcat学习(二)------Tomcat原理详解及请求过程

    Tomcat: Tomcat是一个JSP/Servlet容器.其作为Servlet容器,有三种工作模式:独立的Servlet容器.进程内的Servlet容器和进程外的Servlet容器. Tomcat ...

  8. ARM指令集详解--汇编

    1.       汇编 1.1.    通用寄存器 通用寄存器 37个寄存器,31个通用寄存器,6个状态寄存器,R13堆栈指针sp,R14返回指针,R15为PC指针, cpsr_c代表的是这32位中的 ...

  9. 详解BLE连接建立过程

    同一款手机,为什么跟某些设备可以连接成功,而跟另外一些设备又连接不成功?同一个设备,为什么跟某些手机可以建立连接,而跟另外一些手机又无法建立连接?同一个手机,同一个设备,为什么他们两者有时候连起来很快 ...

随机推荐

  1. Hadoop--初识Hadoop

    什么是Hadoop? 搞什么东西之前,第一步是要知道What(是什么),然后是Why(为什么),最后才是How(怎么做).但很多开发的朋友在做了多年项目以后,都习惯是先How,然后What,最后才是W ...

  2. 如何一步步把网站Retina优化

    随着高清屏幕.高分辨率屏幕越来越流行,例如MacBook Retina机型.iPad Air系列,这些新生机器有着很高的PPI,对网页的清晰度要求很高,所以越来越多的站长都不得不面临一个问题,那就是把 ...

  3. 系统分层 manager层意义

    manager用于控制事务,通常是这么说的,但是如果把事务空指针service可以的,但是有些时候,service加了Transaction注解之后,在加别的注解,可能导致Transaction失效. ...

  4. RANSAC算法详解

    给定两个点p1与p2的坐标,确定这两点所构成的直线,要求对于输入的任意点p3,都可以判断它是否在该直线上.初中解析几何知识告诉我们,判断一个点在直线上,只需其与直线上任意两点点斜率都相同即可.实际操作 ...

  5. Photoshop CS6 基础知识

                                                                  Photoshop CS6  基础知识 新建  练习 宽度72, 像素厘米 ...

  6. javascript 数据结构和算法读书笔记 > 第二章 数组

    这章主要讲解了数组的工作原理和其适用场景. 定义: 一个存储元素的线性集合,元素可以通过索引来任意存取,索引通常是数字,用来计算元素之间存储位置的偏移量. javascript数组的特殊之处: jav ...

  7. 第二章——Serializable的使用(跨进程使用和Intent的传递对象)

    一.Serializable类(JAVA本身具有的) 简介:Serializable是一个接口. 作用:是JAVA提供的序列化接口,实现序列化和反序列化的操作. 二.跨进程使用 1.事前准备 publ ...

  8. 给新建的Cocos2d-x 3.X的Win32工程添加CocoStudio库

    1.我们在VS中找到"解决方案资源管理器", 在解决方案"HelloCocos"上点击右键, 选择添加现有项目. 在弹出的对话框中选择************* ...

  9. laravel5.1框架简介及安装

    最近自己出来实习了,进入了一个新的环境,不仅是生活中,在代码和架构中也完全是一个新的架构.由于公司使用laravel5.1框架,所以最近学习了laravel5.1框架,好了接下来就简单介绍一下lara ...

  10. logstash indexer和shipper的配置

    [elk@zjtest7-frontend config]$ cat logstash_agent.conf input { file { type => "zj_nginx_acce ...