2.函数原型: int function(int i)
现在有了参数了,也有了返回值了,相对来说更比较复杂了。这里就得引入%esp寄存器值的变化了,不然就难以把问题分析清楚了,如果想形象一点地描述那就画图,自己画个图根据我的数据变化一起分析吧。看看一段简单的C代码:
// C Code
int function(int i)
{
return 2 * i;
}
int main(void)
{
int j = function(10);
return 0;
}
之所以些这么简单只是为了我们分析问题的方便,懂得个原理就算是复杂的其实稍微再分析一下也就懂了。我们从main开始分析吧:
main:
pushl %ebp
movl %esp, %ebp
subl $24, %esp
andl $-16, %esp
movl $0, %eax
addl $15, %eax
addl $15, %eax
shrl $4, %eax
sall $4, %eax
subl %eax, %esp #到这里其实和前面的例子基本一样,就不分析了
movl $10, (%esp)
call function
movl %eax, -4(%ebp)
movl $0, %eax
leave
ret
看看上面的汇编代码,和前面一样的不分析。但是其中有句不一样:subl $24, %esp ; 因为主函数里有两个临时变量i, j;这里为了有足够的空间留给临时变量所以干脆在堆栈里腾出24个字节空间。在看看下面的代码:
movl $10, (%esp) #====> %esp = 800, (800) = 10
,其中800是我们假设的地址值,(800)表示地址800的内容这里的(%esp)指的是%esp地址里的内容,
刚才我们假设这时候%esp的值是800, 那么地址为800的内容就是10了。执行函数调用了,注意在调用函数前其实是先把函数调用指令
call之后的地址压栈,也就是call之后那条指令的IP值压栈,所以这时候 %esp =
796;这里要弄明白为什么要把下条指令地址压栈,假设如果不把IP值压栈,那么当函数调用完毕后怎么能找到函数调用时的地址呢?也就是说如果没把IP压
栈,那么函数调用完之后就回不到原来的执行地址了,就会造成程序执行顺序的错误!
下面列出函数function的汇编代码:
function:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax
addl %eax, %eax
popl %ebp
ret
pushl %ebp; 经过这条指令后 %esp值减4,所以这时候%esp值是792。下面这句:
movl %esp, %ebp #==============> %ebp = 792, %esp = 792, (792) = %ebp ;其中(792)表示地址792的内容
movl 8(%ebp), %eax #========> %eax = 10
上面这句很多人可能不明白了,8(%ebp)指的是什么?8(%ebp)等于 : (%ebp + 8) ,这里注意,%ebp + 8
是表示一个地址值,加上括号表示存储在该地址上的内容。
所以8(%ebp)其实就是地址为800的值,看前面地址800的值刚好是10!所以这句其实是把10复制给%eax寄存器.
addl %eax, %eax #======> %eax = 20
相当于2 * %eax, %eax这时候等于20了,刚好是实现了C代码中的 (2 * i);
popl %ebp #=========> 恢复%ebp寄存器的值, %esp这时候等于796
ret #=========> 函数调用完毕返回,这句其实是把刚才压栈的IP值弹出栈,执行这条指令后 %esp = 800
# 800!想想我们在调用函数的时候%esp也是800啊!这就是实现了“清栈”了,就是把调用函数所在的栈清除了!
好了,函数 function的汇编代码分析完了,现在回头继续看看main函数里的下一条指令了。接下来是这句:
movl %eax, -4(%ebp)
%eax寄存器存放的是什么?看function函数的代码,可以知道其实就是(2 *
i)的值,所以返回值其实是通过%eax来传递的!传递到-4(%ebp)里去了,-4(%ebp) = (%ebp - 4);
-4(%ebp)到底是什么呢?看看C代码,返回值传给变量j,那么-4(%ebp)会不会就是j呢?答案是肯定的!我们先看看%ebp的值是什么。看看
main函数的汇编代码,可以得出,%ebp其实指向了main函数的栈底部,但记不记得前面说的subl $24,
%esp是为临时变量而留出的空间?没错,-4(%ebp) 就是存储在临时变量区域!也就是变量 j 了。
相关文章
- C#委托(delegate)的常用方式- 委托的定义 // 委托的核心是跟委托的函数结构一样 public delegate string SayHello(string c); public delegate string SayHello(string c);:定义了一个公共委托类型 SayHello,该委托接受一个 string 类型的参数 c,并返回一个 string 类型的值。 Main 方法 static void Main(string args) { // 本质上其实就是把方法当作委托的参数 SayHello sayC = new SayHello(SayChinese); Console.WriteLine(sayC("欢迎大家")); SayHello sayE = new SayHello(SayEgnlish); Console.WriteLine(sayE("Welcome to")); // 简单的写法:必须类型一样 SayHello s1 = SayChinese; SayHello s2 = SayEgnlish; Console.WriteLine(s1("好好好")); Console.WriteLine(s2("Gooood")); // 最推荐 SayHello ss1 = con => con; Console.WriteLine(ss1("niiiice")); // 匿名委托:一次性委托 SayHello ss3 = delegate(string s) { return s; }; Console.WriteLine(ss3("说中国话")); } 常规实例化委托 SayHello sayC = new SayHello(SayChinese);:创建了一个 SayHello 委托的实例 sayC,并将 SayChinese 方法作为参数传递给委托的构造函数。 Console.WriteLine(sayC("欢迎大家"));:通过委托实例调用 SayChinese 方法,并输出结果。 同理,SayHello sayE = new SayHello(SayEgnlish); 和 Console.WriteLine(sayE("Welcome to")); 是对 SayEgnlish 方法的委托调用。 简化的委托赋值方式 SayHello s1 = SayChinese; 和 SayHello s2 = SayEgnlish;:当委托类型和方法签名一致时,可以直接将方法赋值给委托变量,无需使用 new 关键字。 Console.WriteLine(s1("好好好")); 和 Console.WriteLine(s2("Gooood"));:通过委托实例调用相应的方法。 使用 Lambda 表达式实例化委托 SayHello ss1 = con => con;:使用 Lambda 表达式创建委托实例 ss1,con => con 表示接受一个参数 con 并返回该参数本身。 Console.WriteLine(ss1("niiiice"));:通过委托实例调用 Lambda 表达式。 匿名委托 SayHello ss3 = delegate(string s) { return s; };:使用匿名委托创建委托实例 ss3,delegate(string s) { return s; } 是一个匿名方法,直接在委托实例化时定义了方法体。 Console.WriteLine(ss3("说中国话"));:通过委托实例调用匿名方法。 委托引用的方法定义 public static string SayChinese(string content) { return content; } public static string SayEgnlish(string content) { return content; } public static string SayChinese(string content) 和 public static string SayEgnlish(string content):定义了两个静态方法,分别接受一个 string 类型的参数 content,并返回该参数本身。这两个方法的签名与 SayHello 委托一致,可以被 SayHello 委托引用。 常规的委托实例化、简化的赋值方式、Lambda 表达式和匿名委托。委托在 C# 中是一种强大的机制,它允许将方法作为参数传递,实现了代码的灵活性和可扩展性。
- 关于C语言中函数调用和参数传递机制的探讨(二 .传递一个参数)
- 关于C语言中函数调用和参数传递机制的探讨
- 关于C语言中函数调用和参数传递机制的探讨(三 .传递多个参数等)