昨天室友拿一个面试题为难我,问我C/C++函数调用是怎么一个流程。这问题实在简单,然而有一本什么面试宝典却说的前后不一,漏洞重重。室友尽信于书,非与我分个高低。单从机制本身来说,公说公有理,婆说婆有理,于是我就用了一个简单的实验才勉强说清楚。在此也顺便总结一下,从汇编的角度介绍一下函数调用过程。





当调用者比如h调用某个函数f时,从编译器或者汇编语言角度来看,主要分以下几个步骤进行:
- h将实参按照从右向左的顺序一个个压入stack中。
- 执行一个转移指令call f
- f执行完函数体后,将返回值传入寄存器AX/EAX/RAX中。
- f执行转移指令ret
- h将实参从stack中一个一个弹出。
具体来说,从内存的角度看,函数h调用f时,Stack是按下面步骤发生变化的:
- 实参按照从右向左的顺序一个一个进入stack中。
- 函数调用指令之后的“下一条指令地址”进入stack中。
- 函数f中的局部变量加入到stack中。
- 函数f中的局部变量从Stack中弹出。
- “下一条指令地址”从stack中弱出,流入程序计数器寄存器IP中。
- 寄存器AX/EAX/RAX中的值流入到stack中h的局部变量(或者全局变量等)中。
- 调用函数f时的实参从stack中弹出。
那么,到底是谁将“下一条指令地址”放入stack中的呢?当然是调用者h了。其实这个功能是一条汇编指令call实现的,而不是简单的用push/pop/mov指令实现的。CALL指令的执行可以视为做了以下工作:
- 将“下一条指令地址”压入stack。
- 改变IP的值为被调用的函数的地址。
想来个直观点的说明,最好还是通过一个小程序。昨天用GCC已经做过测试,由于版本比较新,它做的优化太多了,比如它尽量使用寄存器进行参数传递而非Stack,所以介绍起来比较麻烦。并且GCC使用的AT&T汇编格式比较难懂,还是用WINDOWS下都熟悉的MASM格式的汇编来列一下吧。同时为了清晰,少费点口舌,就用可视化的工具VC++6.0来介绍。
首先,假设程序代码如下(很简单的):
int f(int a, int b)
{
return a*b;
}
int main(int argc, char *argv[])
{
int x = 0;
x = f(5,6);
++x;
return 0;
}
{
return a*b;
}
int main(int argc, char *argv[])
{
int x = 0;
x = f(5,6);
++x;
return 0;
}
编译完的汇编代码不再列出。对它调度跟踪一下,一切问题都有了着落。比如先把断点设在x=f(5,6)的地方,执行到该位置后,各个寄存器的值如“Resisters”窗口所示,此时的断点以及对应的汇编代码如下:


接着执行,执行到call那条指令时,内存内容及相应寄存器的值如下面的图。Memory窗口显示了当前Stack从顶部开始的内存内容。最顶的是参数5(占4个字节,从低到高),然后是6,这两个是传给f的实参。此时,“下一条指令地址”也就是紧挨着的add指令的地址(0x401078)并没有放到stack中。由此可见,“下一条指令地址”是第一个进栈的这种说法是不对的。

然后接着执行,采用step into(VC6.0中对应F11),也就是仅执行call这条指令,而不执行f中的任何指令。执行后如下图。可以看出,此时寄存器ESP的值变了,向下移动了4个字节,也就是stack中新插入了一个4字节的整数(其实它是一个内存地址)。这个新进来的地址是0x00401078,对应main函数中的add那条指令,也调用时的就是“下一条指令地址”。现在很清楚了,将“下一条指令地址压stack”是CALL指令的功能,是硬件干的,而不是软件。

然后要说明的就是函数f返回过程了。当程序执行到RETURN语句时,对应的内存和寄存器状态如下。可以看出,RET指令执行之前,“下一条指令地址”还在stack中,同时EAX的值0x1E也就是30就是函数f的返回值。用EAX传递返回值是编译器的一个习惯,能不能说是标准我不太确定。反正GNU系列的编译器和微软的都是这么干的。

最后,当f中的RET执行完后,会形成如下格局。与上图对比,会发现,stack顶的值跑到EIP里面去了。stack中仅剩下之前的两个实参!所以,“下一条指令地址”是第一个出栈的,而不是最后一个。接下来的add指令的意思是将ESP加8,其实就是将之前放入stack的两个实参从stack中移除。函数调用也到此结束。

posted @ 2009-09-05 15:50 黄鹏 阅读(1928) 评论(3) 编辑
新学期刚开始,就业已经拉开了序幕。
今年遇到的第一个招聘公司是迅雷。当时由于时间紧张,手头事情比较多,简历还没有做完。后来参加了霸王一笔,全是基本语言语法,答的很惨。心想一定被拒了,然而考完几个小时,也就是凌晨不到一点的时候,收到了迅雷的二笔通知,心中狂欢。
二笔安排在第二天晚上7点到9点,三个程序题(30分+30分+40分),2个小时,答的够累的。两张答题纸用完我又要了三张,最后还是没写完,也只好就此收尾了。说实话题目很容易的,自己答的很有自信。然而,我却没有收到面试通知。
我在想,什么原因把我给拒了?真的是笔试没答好吗?想了很久,觉得可能是简历上工作地点的原因,因为我对地点的要求是北京和上海,没有深圳。说实话,我还是很想去迅雷的。于是就去了霸王面。但是不凑巧,所有去霸王面的,都只是被拒在了门外。没办法,只能恭喜闯关成功者了。
昨天晚上回来,室友又拿笔试题来和我探讨,猛然发现,自己把题意搞错了!心中特别惭愧,同时也输的心服口服。
最后,把迅雷笔试题贴出来留个纪念吧。
(下面来自CSDN博客,转载请标明出处:http://blog.csdn.net/morre/archive/2009/09/01/4509390.aspx)
迅雷2010校园招聘吉林大学第二次笔试题
答题时间: 2小时,请将答案写在答题纸上
一. 有n个文件的长度记载在一个无符号64 位整数数组中unsigned __int64 file_length[n],把这n 个文件从逻辑上按序首尾拼接在一起形成一个逻辑上的大文件,然后以每块长度为unsigned block_length把这个逻辑上的大文件划分成大小相等的数据块(当然,最后一块有可能比block_length小),请定义和实现一个函数,把边界块的序号集合返回给函数的调用者(第一个数据块序号为0)。
注:边界块指的是跨多个文件的数据块。(30分)
二. 请实现一个函数,把两个从大到小的有序链表合并成一个链表,新的链表是一个从小到大的有序链表。
struct list
{
int value;
list* next;
};
list * merge (list *list1_head, list *list2_head);
(30分)
三. 如果两个英文单词,组成它们的字符集合相同,而且相同字符出现的次数也相同,则称这两个词匹配:比如说:同”abbc”与词”babc”是匹配的。有一个词典,存储在字符串数组const char* dictionary[n]中,数组的每一个元素是一个词。对于任意给出的句子。句子中的单词使用空格分割。请实现以下函数,判断句子中是否有词和词典中的词匹配。
bool is_matching( const char* dictionary[], int n, const char* sentence);
(40分)
注意:这一题需要先描述思路,再写程序,没写思路扣10分。
答题时间: 2小时,请将答案写在答题纸上
一. 有n个文件的长度记载在一个无符号64 位整数数组中unsigned __int64 file_length[n],把这n 个文件从逻辑上按序首尾拼接在一起形成一个逻辑上的大文件,然后以每块长度为unsigned block_length把这个逻辑上的大文件划分成大小相等的数据块(当然,最后一块有可能比block_length小),请定义和实现一个函数,把边界块的序号集合返回给函数的调用者(第一个数据块序号为0)。
注:边界块指的是跨多个文件的数据块。(30分)
二. 请实现一个函数,把两个从大到小的有序链表合并成一个链表,新的链表是一个从小到大的有序链表。
struct list
{
int value;
list* next;
};
list * merge (list *list1_head, list *list2_head);
(30分)
三. 如果两个英文单词,组成它们的字符集合相同,而且相同字符出现的次数也相同,则称这两个词匹配:比如说:同”abbc”与词”babc”是匹配的。有一个词典,存储在字符串数组const char* dictionary[n]中,数组的每一个元素是一个词。对于任意给出的句子。句子中的单词使用空格分割。请实现以下函数,判断句子中是否有词和词典中的词匹配。
bool is_matching( const char* dictionary[], int n, const char* sentence);
(40分)
注意:这一题需要先描述思路,再写程序,没写思路扣10分。
posted @ 2009-09-05 13:45 黄鹏 阅读(772) 评论(0) 编辑

