本文以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,也就是参数正向压栈,汇编级别处理起来就特别麻烦了。

&spm=1001.2101.3001.5002&articleId=7075986&d=1&t=3&u=1966e247fee543eebca7c98433c1037c)
5651

被折叠的 条评论
为什么被折叠?



