精华内容
下载资源
问答
  • 本关的目标是让读者了解并掌握函数调用的相关知识。 相关知识 定义函数与调用函数的顺序 在定义了函数之后,就可以调用该函数了,但是在Python中我们要注意一个关键点,就是Python允许前向引用,即在函数定义...

    任务描述

    函数被定义后,本身肯定不会自动执行的,只有在被调用后,函数才会被执行,得到相应的结果。

    本关的目标是让读者了解并掌握函数调用的相关知识。

    相关知识

    定义函数与调用函数的顺序

    在定义了函数之后,就可以调用该函数了,但是在Python中我们要注意一个关键点,就是Python不允许前向引用,即在函数定义之前,不允许调用该函数。例如:

    print plus(1,2)
    
    def plus(a,b):
        return a+b

    运行结果为:

    NameError: name 'plus' is not defined

    从报错结果可以看出,名字为plus的函数还没进行定义(虽然我们是在后面进行了定义)。所以当我们在调用函数时,一定要确定在调用之前进行了函数定义。

    正确使用参数

    我们要在调用函数时,需要正确调用函数的名称和参数,例如我们定义了一个加法函数:

    def plus(a,b):
        return a+b

    我们在调用plus()函数时,如果传入的参数类型不对,会报TypeError错误。而且有时我们传入的参数类型不是规定类型的话,就算调用函数运行结果没有报错,也会产生逻辑错误。例如:

    #定义plus函数,作用是求两个正整数之和
    def plus(a,b):
        return a+b
    
    #调用plus函数,参数类型为'1','2'
    print(plus('1','2'))

    输出结果:

    12

    虽然上述例子的程序运行结果没有报错,但是结果却与我们的预期不符,因为我们的本意是调用plus()函数实现两个整数的加法,但是如果我们传入的是字符串类型的数值时,结果就是两个字符串的拼接。

    所以这个例子告诉我们一定要注意传入参数的类型。

    当我们传入正常类型的参数时,传入的参数个数不一致时,也会报TypeError错误,例如:

    #定义plus函数,作用是求两个正整数之和
    def plus(a,b):
        return a+b
    
    #调用plus函数,参数为1,2,3
    print(plus(1,2,3))

    报错:

    TypeError: plus() takes 2 positional arguments but 3 were given

    报错原因显示,因为plus()函数允许有且仅有2个参数,但是却在调用时传入了3个参数,所以程序报错。

    函数的调用十分重要,只有学会正确调用函数,才会得到正确的函数运行结果。

    编程要求

    本关的编程任务是补全src/Step2/func_call.py文件的代码,实现相应的功能。具体要求如下:

    • 定义一个函数,实现对输入的数值列表进行从小到大的顺序排序;
    • 输出排序后的数值列表。

    测试说明

    本关的测试文件是src/Step2/func_call.py,测试过程如下:

    1. 平台自动编译生成func_call.exe;
    2. 平台运行func_call.exe,并以标准输入方式提供测试输入;
    3. 平台获取func_call.exe输出,并将其输出与预期输出对比。如果一致则测试通过,否则测试失败。

    以下是平台对src/Step2/func_call.py的样例测试集:

    测试输入:
    103,47,21,34,11,2,5,88,13
    预期输出:
    [2, 5, 11, 13, 21, 34, 47, 88, 103]

    测试输入:
    12,31,0,23,25,109,77,3
    预期输出:
    [0, 3, 12, 23, 25, 31, 77, 109]

    测试输入:
    6,4,11,34,12,1,4
    预期输出:
    [1, 4, 4, 6, 11, 12, 34]

    测试输入:
    5,4,3,2,1
    预期输出:
    [1, 2, 3, 4, 5]

    开始你的任务吧,祝你成功!

    保持对事业的努力,事业比幻想中的金钱要现实得多。这是必须牢记的原则。

    如果你觉得这一关的内容对你有帮助,请你在下面点赞。

     

    参考答案

    #coding=utf-8
    
    #输入数字字符串,并转换为数值列表
    a = input()
    num1 = eval(a)
    numbers = list(num1)
    
    # 请在此添加代码,实现编程要求
    #********** Begin *********#
    result = sorted(numbers)
    
    print(result)
    
    #********** End **********#

     

    展开全文
  • 浅谈C语言函数调用与系统调用

    千次阅读 2019-11-12 09:55:50
    1. 函数调用和系统调用概述 1.1 定性的去区分函数调用和系统调用 很多初学C语言的同学,亦或者开发中很少接触系统底层的同学可能会认为函数调用以及系统调用是一回事。因为在应用程序,两者都被抽象成接口去给应用...

    1. 函数调用和系统调用概述

    1.1 定性的去区分函数调用和系统调用

    很多初学C语言的同学,亦或者开发中很少接触系统底层的同学可能会认为函数调用以及系统调用是一回事。因为在应用程序,两者都被抽象成接口去给应用程序调用。其实函数调用和系统调用还是有区别,我们通过下图先有个全局的了解!

    图1-1 系统功能模块关联图

     

    从图1-1 我们可以知道应用程序访问内核,主要通过两种方式:中断和系统调用接口。

    其一,中断是置于程序流程之外的,所以这种方式并不是我们访问内核的普遍方式,更多的是因为程序异常而引起的中断;

    其二,就是系统调用接口,例如使用 open、read 以及 write 去操作文件数据(相当于去访问硬盘)。

    而函数调用是无法直接访问内核的,函数调用需要访问内核,也只能通过系统调用的方式。

    所以,我们可以通过这种方式定性地去区分函数调用和系统调用: 接口直接访问内核的属于系统调用,其他则为函数调用

    1.2 为什么要封装好系统调用

    我们知道,32位 linux系统为了保证程序独立性,给每一个进程分配独立的4G地址空间(虚拟内存空间),但是4G地址空间又被划分为两个空间:用户空间以及内核空间,如下图:

    图1-2 进程地址空间划分图

    用户空间,存储的是一些进程常见的数据,例如代码、变量数据、堆区和栈区等,需要注意的是内核空间,虽然说系统给每一个进程都分配1G大小的内核空间,但是实际上,内核空间是操作系统中进程公用的。公用就会带来一个现实性的问题,就比如我们可以在自己的房间干任何事情(用户空间),去不能在公共场合(内核空间)为所欲为,活动在公共场合(内核空间),我们的行为会受到限制,对于操作系统也一样道理,内核空间属于进程公用,进程去访问内核必须保证不会给其他进程造成影响。

    如何去保证进程操作系统中公用的内核空间不会出现非法操作,打个比方说:如何保证银行内部数据不被非法操作,银行是提供一个ATM给你,你只能选择上面的接口操作。操作系统也“学习”了这样的模式,封装接口,保证数据操作正确性!

    不仅仅应用程序无法直接访问内核空间,只能通过系统调用访问内核空间,而且内核对用户空间的数据的不信任,对于通过系统调用的传递过来的数据也会做相应的检查,确保内核安全,确保内核安全是必须的,内核属于系统的软件核心,内核崩溃会导致整个软件系统的崩溃。为了保证应用程序传递的数据合法性以及内核数据的绝密性,通常会使用copy_from_user去检验和拷贝传递过来的数据,使用copy_to_user()检验和拷贝数据给应用程序。

    2.函数调用

    2.1 什么叫做函数调用

    int add(int a, int b){
    
        return(a + b);
    }
    
    int main(){
    
        int sum;
        
        sum = add(1 + 2);
        
        return 0;
    }

    如上是一个最简单的函数调用过程,了解什么叫函数调用,我们可以通过了解什么是函数?

    函数(function)是完成特定任务的独立程序代码单元。

                                                                                                                                                         ----《C Primer Plus》

    再来了解为啥需要使用函数?

    首先,使用函数可以省去编写重复代码的苦差。如果程序多次完成某一项。如果程序需要多次完成某项任务,那么只需编写一个适合的函数,就可以在需要时使用这个函数,或者在不同的程序中使用该函数,就像许多程序使用putchar() 一样。

    其次,即使程序只完成某项任务一次,也值得使用函数。因为函数使程序更加模块化,从而提高了程序代码的可读性,更方便后期修改、完善。

                                                                                                                                                         ----《C Primer Plus》

    函数调用是很好理解的,提高代码重用性以及模块性,调用相对应函数,得到某种结果。

    2.1 函数调用的过程

    函数调用会发生函数压栈以及出栈过程,具体可以参考如下博文和视频(博主对汇编不熟悉,所以就不具体分析了,以免误人子弟!)。

    浅析函数的调用过程

    汇编中的函数调用中栈的工作过程

    3.系统调用

    3.1 什么叫做系统调用

    为了和用户空间上运行的进程进行交互,内核提供了一组接口。透过该接口,应用程序可以访问硬件设备和其他操作系统资源。这组接口在应用程序和内核之间扮演了使者的角色,应用程序发送各种请求,而内核负责满足这些请求(或者让应用程序暂时搁置)

                                                                                                                                             ----《Linux内核设计与实现》

    3.2 系统调用的过程

    系统调用和函数调用在应用程使用几乎没有差别,但是内部实现过程是完全不同的,函数调用是通过函数入口地址直接跳转函数服务程序,而系统调用并非直接跳转系统调用服务程序,因为内核地址对于应用程序是不可用的,需要通过中断的方式进入内核态,再通过一系列的操作找到对应的服务程序。

    我们可以通过下图有一个了解:

    图3-1 系统调用过程流程图

    希望更详细的了解系统调用过程可以看看下面这篇博文:

    ioctl系统调用过程(深入Linux(ARM)内核源码)

    展开全文
  • linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。Shell 函数定义的语法格式如下: [function] funname [()]{ 函数体 [return int;] } 格式1: 简化写法,写 function 关键字: 函数名(){ ...

    分享知识 传递快乐

     

    1、函数定义

    linux shell 可以用户定义函数,然后在shell脚本中可以随便调用。Shell 函数定义的语法格式如下:

    [function] funname [()]{
        函数体
        [return int;]
    }

    格式1:

    简化写法,不写 function 关键字:

    函数名(){
        函数体
    }

    格式2:

    这是标准写法,也推荐大家使用的写法:

    function 函数名(){
    	命令序列
    }

    这样方便做到一看就懂。

    格式3:

    如果写了 function 关键字,也可以省略函数名后面的小括号:

    function 函数名{
    	命令序列
    }


    说明:

    • function 是 Shell 中的关键字,专门用来定义函数;可以带function funname () 定义,也可以直接funname () 定义,不带任何参数;
    • funname 是函数名;
    • 函数体 是函数要执行的代码,也就是一组语句;
    • return int 表示函数的返回值,其中 return 是 Shell 关键字,专门用在函数中返回一个值;这一部分可以写也可以不写,如果不加,将以最后一条命令运行结果,作为返回值。

     

    2、函数变量

    Shell 函数的变量不像其它语言中需要在变量前定义变量类型什么的,更没有结果符号等。语法:

    num=1

    变量作用域

    局部变量:作用域是函数的生命周期;在函数结束时被自动销毁。定义局部变量的方法:

    local VAR=VALUE

    本地变量:作用域是运行脚本的shell进程的生命周期;因此,其作用范围为当前shell

    示例一:

    #!/bin/bash
    
    #在函数外定义本地变量
    var="Hello,World"
    
    function show() {
        #在函数内改变变量内容
        var="Hi,var is changed"
    }
    echo "$var"
    show
    echo "$var"

    输出结果:

    [guest@localhost shell]$ ./tempsh.sh
    Hello,World
    Hi,var is changed

    结果显示在调用函数后,原有的本地变量var被替换了。还好这个变量并不是重要的部分,想想若是PATH被替换了,那么这个函数的罪过就大了。因此我们如何即调用函数中定义的变量同时又不对本地变量造成任何影响呢?局部变量的出现就是为了解决这个问题。

    下面看看在使用了局部变量后的效果。

    #!/bin/bash
    #在函数外定义本地变量
    var="Hello,World"
    
    function show() {
        #在函数内改变变量内容
        local var="Hi,var is changed"
        echo "$var"
    }
    echo "$var"
    show
    echo "$var"

    输出结果

    [guest@localhost shell]$ ./tempsh.sh
    Hello,World
    Hi,var is changed
    Hello,World

    该实验结果说明,使用局部变量后,函数体中出现的变量作用范围只存在于当前函数生命周期。

     

    3、函数调用

    定义函数的代码段不会自动执行,而是在调用时执行;在函数定义好后,用户可以在shell 中直接调用,调用时不用带上();调用 Shell 函数时可以给它传递参数,也可以不传递。如果不传递参数,直接给出函数名字即可。

    示例一:

    #!/bin/bash
    
    # 函数定义
    function show(){
        echo "Hello word"
    }
    
    # 函数调用
    show

    输出结果:

    Hello word

    函数调用成功。上边的例子是把函数把在脚本上边,那么如果放函数放在下边会怎样呢?

    无非就两种结果:1成功,2失败

    下面我们举例测试一下:

    示例二:

    #!/bin/bash
    
    # 函数调用
    show
    
    #函数定义
    function show(){
        echo "Hello word"
    }

    输出结果:

    ./tempsh.sh:行4: show: 未找到命令

    系统报错,为啥?为什么会报错呢?

    首先,脚本的执行顺序是从上到下顺序执行的,因此会先执行show,通过定义的环境变量$PATH定义的路径找不到show对应的命令因此报“show:未找到命令”。

    我们在终端命令行中输错命令报错也是这个原因。终端命令行默认会将最左面输入的内容当做命令,因此若是错误的命令,不是命令的命令等内容都会报错。

    通过上面的对比,我们至少知道函数的调用若是在同一个脚本中,调用操作需要在定义的函数后面。

     

    4、函数传参

    函数传参调用语法:

    函数名 参数1 参数2 ....

    如果传递参数,那么多个参数之间以空格分隔:

    funname param1 param2 param3

    不管是哪种形式,函数名字后面都不需要带括号。和其它编程语言不同的是,Shell 函数在定义时不能指明参数,但是在调用时却可以传递参数,并且给它传递什么参数它就接收什么参数。

    在Shell中,调用函数时可以向其传递参数。在函数体内部,函数中的变量均为全局变量,没有局部变量,若使用局部变量需要在变量前加上 local,通过 $n 的形式来获取参数的值,例如,$1表示第一个参数,$2表示第二个参数....

    示例:

    #!/bin/bash
    
    function show(){
        echo "第一个参数为 $1 !"
        echo "第二个参数为 $2 !"
        echo "第十个参数为 $10 !"
        echo "第十个参数为 ${10} !"
        echo "第十一个参数为 ${11} !"
        echo "参数总数有 $# 个!"
        echo "作为一个字符串输出所有参数 $* !"
    }
    
    show 0 1 2 3 4 5 6 7 8 9 10 11

    输出结果:

    第一个参数为 0 
    第二个参数为 1 
    第十个参数为 10 
    第十个参数为 9 
    第十一个参数为 10 
    参数总数有 12 个
    作为一个字符串输出所有参数 0 1 2 3 4 5 6 7 8 9 10 11 

    注意:$10 不能获取第十个参数,获取第十个参数需要${10}。当n>=10时,需要使用${n}来获取参数。

    另外,还有几个特殊字符用来处理参数:

    参数处理说明
    $#传递到脚本的参数个数
    $*以一个单字符串显示所有向脚本传递的参数
    $$脚本运行的当前进程ID号
    $!后台运行的最后一个进程的ID号
    $@与$*相同,但是使用时加引号,并在引号中返回每个参数。
    $-显示Shell使用的当前选项,与set命令功能相同。
    $?显示最后命令的退出状态。0表示没有错误,其他任何值表明有错误。

     

    5、函数返回值

    退出状态码

    在介绍函数返回值前先了解一下跟函数返回值有关的状态退出码。

    Shell 中运行的每个命令都使用退出状态码(exit status)来告诉shell它完成了处理。退出状态码是一个0-255之间的整数值,在命令结束运行时由命令传给shell。你可以捕获这个值并在脚本中使用。

    如何查看退出状态码呢?

    Linux提供了 $? 专属变量来保存上个执行的命令的退出状态码。你必须在你要查看的命令之后马上查看或使用 $? 变量。它的值会变成Shell中执行的最后一条命令的退出状态码。

    退出状态码大体分两种:

    • 一种是命令正确执行的状态码,该状态码为:0
    • 一种是命令错误执行的状态码,为1-255

     Linux退出状态码

    状态码描述
    0命令成功结束
    1通用未知错误
    2误用shell命令
    126命令不可执行
    127没找到命令
    128无效退出参数
    128+xLinux信号x的严重错误
    130命令通过Ctrl+C终止
    255退出状态码越界

    在脚本中也可以指定退出状态码的值,通过命令exit实现。

    状态码取值范围为0-255,如果在指定的状态码大于255,那么shell会通过模(模就是256)运算得到相应的退出状态码。

    示例一:

    成功的

    #!/bin/bash
    
    # 函数定义
    function show(){
        echo $(date +%Y%m%d)
    }
    show
    echo $?

    输出结果:

    0

    示例二:

    失败的

    #!/bin/bash
    
    SYS_DATE=$(date +%Y%m%d)
    
    echo $SYS_DATE
    
    # 函数定义
    function show(){
        log=`lt`
    	echo log
    }
    show
    echo $?

    输出结果:

    [guest@localhost shell]$ ./tempsh.sh 
    20191123
    ./tempsh.sh:行10: lt: 未找到命令
    log
    0
    

    这次,由于函数最后一行命令正确执行,函数的退出状态码就是0,尽管函数中有一条命令没有成功运行。

    使用函数的默认退出状态码是很危险的,幸运的是return命令可以解决这个问题。

    示例:

    #!/bin/bash
    
    SYS_DATE=$(date +%Y%m%d)
    
    echo $SYS_DATE
    
    # 函数定义
    function show(){
        log=`lt`
    	echo log
    	return 2
    }
    show
    echo $?

    输出结果:

    [guest@localhost shell]$ ./tempsh.sh 
    20191123
    ./tempsh.sh:行10: lt: 未找到命令
    
    2
    

    还是使用相同的函数,在函数最后加上return指定的状态码2。

     

    函数返回值

    Shell函数返回值,常用的两种方式:return、echo。

     

    1)return 语句

    Shell函数的返回值,可以和其他语言的返回值一样,通过return语句返回,return只能用来返回整数值。

    示例一:

    #!/bin/bash
    
    function getResultFun(){
        echo "这是我的第一个 shell 函数!"
        return `expr 1 + 1`
    }
    
    getResultFun
    echo $?

    输出结果:

    这是我的第一个 shell 函数!
    2

    Shell 函数返回值只能是整形数值,一般是用来表示函数执行成功与否的,0表示成功,其他值表示失败。用函数返回值来返回函数执行结果是不合适的。如果return某个计算结果,比如一个字符串,往往会得到错误提示:“numeric argument required”。

    如果一定要让函数返回一个或多个值,可以定义全局变量,函数将计算结果赋给全局变量,然后脚本中其他地方通过访问全局变量,就可以获得那个函数“返回”的一个或多个执行结果了。

    示例:

    #!/bin/sh
    
    function getStr(){
    	return "string"
    }
    
    getStr
    echo $?

    输出如下:

    ./test.sh: line 5: return: string: numeric argument required
    255

    可以看到已经提示要求return 整数类型,真实返回值是255。当面对这种问题怎么解决呢?

    别急,断续看下去你就会找到你想要的答案了。

     

    2)echo 语句

    echo是通过输出到标准输出返回,可以返回任何类型的数据。

    示例:

    #!/bin/sh
    
    function test()  {
    	echo "arg1 = $1"
    	if [ $1 = "1" ] ;then
    		echo "1"
    	else
    		echo "0"
    	fi
    }
    
    echo
    echo "test 1"
    test 1
    
    echo
    echo "test 0"
    test 0
    
    echo
    echo "test 2"
    test 2

    输出结果:

    test 1
    arg1 = 1
    1
    
    test 0
    arg1 = 0
    0
    
    test 2
    arg1 = 2
    0

     

    3)函数返回值总结

    学习了上面的函数返回值的操作后我们下面做个知识总结,我们先看一用例:

    #!/bin/bash
    
    function getResultFun(){
        echo "这是我的第一个 shell 函数!"
        return `expr 1 + 1`
    }
    
    getResultFun
    echo $?
    
    function getResultFun2(){
     echo "这是我的第二个 shell 函数!"
     expr 1 + 1
    }
    
    getResultFun2
    echo $?
    
    getResultFun
    echo 在这里插入命令!
    echo $?

    输出结果:

    这是我的第一个 shell 函数!
    2
    这是我的第二个 shell 函数!
    2
    0
    这是我的第一个 shell 函数!
    在这里插入命令!
    0

    这是为什么?

    因为调用 getResultFun2 后,函数最后一条命令 expr 1 + 1 得到的返回值($?值)为 0,意思是这个命令没有出错。所有的命令的返回值仅表示其是否出错,而不会有其他有含义的结果。

    第二次调用 getResultFun 后,没有立即查看 $? 的值,而是先插入了一条别的 echo 命令,最后再查看 $? 的值得到的是 0,也就是上一条 echo 命令的结果,而 getResultFun 的返回值被覆盖了。下面这个测试,连续使用两次 echo $?,得到的结果不同,更为直观:

    #!/bin/bash
    
    function getResult(){
        echo "这是我的第一个 shell 函数!"
        return `expr 1 + 1`
    }
    
    getResult
    echo $?
    echo $?

    输出结果:

    这是我的第一个 shell 函数!
    2
    0

     

    6、获取函数返回值

    我们上面谈到了函数定义、传参、调用、返回结果,那我们如果得到返回结果呢?上面也谈到了一种获取返回结果的方法 $? ,难道只有这一种方式吗?答案肯定不止。

    示例一:

    用 $? 获取返回值,上面已有介绍,在这里就不做介绍了。

    示例二:

    #!/bin/sh
    
    function getStr(){
    	return "string"
    }
    
    #方法一
    echo `getStr` 
    
    #方法二
    echo $(getStr) 

    两种写法的原理一样的,将getStr当成命令执行,然后获取其标准输出。

    示例三:函数传参

    #!/bin/bash
    
    #创建目录
    function createDir(){
    	if [ ! -d $1 ]; then
    		mkdir -p $1
    	fi
    }
    
    DIR="temp/"
    # 两者二先一
    #$(createDir $DIR)
    $(createDir "temp/")

    函数返回值获取的方法总结:

    • 用变量接收函数返回值,函数用echo等标准输出将要返回的东西打印出来。
    • 用 $? 来接收函数的执行状态,但是 $? 要紧跟在函数调用处的后面。

     

     

     

     

     

     

     

     

     

     

    ———————————
    相互学习,共同进步
    如有不足请留言指正

    展开全文
  • 汇编:函数调用

    千次阅读 2019-07-18 18:57:33
    让我们再回忆一下脑海中关于函数调用的概念,也许会是这个样子: 这里的“控制流转移”又是如何发生的呢?在解释这个之前,也许我们需要科普一点有关于汇编的知识。 2. 函数调用中的一些细节说明 2.1 函数调用中的...

    1. 从代码的顺序执行说起

    每一个程序员脑子里应该都有这么一种印象:“程序是顺序执行的”。这个观点其实和我们开篇所讲的cpu的流水线执行过程直接相关。
    让我们再回忆一下脑海中关于函数调用的概念,也许会是这个样子:
    Paste_Image.png

    这里的“控制流转移”又是如何发生的呢?在解释这个之前,也许我们需要科普一点有关于汇编的知识。

    2. 函数调用中的一些细节说明

    2.1 函数调用中的关键寄存器

    2.1.1 程序计数器PC

    程序计数器是一个计算机组成原理中讲过的概念,下面给出一个百度百科中的简单解释

    程序计数器是用于存放下一条指令所在单元的地址的地方。
    当执行一条指令时,首先需要根据PC中存放的指令地址,将指令由内存取到指令寄存器中,此过程称为“取指令”。与此同时,PC中的地址或自动加1或由转移指针给出下一条指令的地址。此后经过分析指令,执行指令。完成第一条指令的执行,而后根据PC取出第二条指令的地址,如此循环,执行每一条指令。

    可以看到,程序计数器是一个cpu执行指令代码过程中的关键寄存器:它指向了当前计算机要执行的指令地址,CPU总是从程序计数器取出当前指令来执行。当指令执行后,程序计数器的值自动增加,指向下一条将要执行的指令。
    在x86汇编中,执行程序计数器功能的寄存器被叫做EIP,也叫作指令指针寄存器。

    2.1.2 基址指针,栈指针和程序栈

    栈是程序设计中的一种经典数据结构,每个程序都拥有自己的程序栈。很重要的一点是,栈是向下生长的。所谓向下生长是指从内存高地址->低地址的路径延伸,那么就很明显了,栈有栈底和栈顶,那么栈顶的地址要比栈底低。对x86体系的CPU而言,其中
    —> 寄存器ebp(base pointer )可称为“帧指针”或“基址指针”,其实语意是相同的。
    —> 寄存器esp(stack pointer)可称为“ 栈指针”。
    在C和C++语言中,临时变量分配在栈中,临时变量拥有函数级的生命周期,即“在当前函数中有效,在函数外无效”。这种现象就是函数调用过程中的参数压栈,堆栈平衡所带来的。对于这种实现的细节,我们会在接下来的环节中详细讨论。

    2.2. 堆栈平衡

    堆栈平衡这个概念指的是函数调完成后,要返还所有使用过的栈空间。这种说法可能有点抽象,我们可以举一个简单的例子来类比:
    我们都知道函数的临时变量存放在栈中。那我们来看下面的代码,它是一个很简单的函数,用来交换传入的2个参数的值:

    void __stdcall swap(int& a,int& b)
    {
      int c = a;
      a = b;
      b = c;
    }
    

    我们可以看到,在这个函数中使用了一个临时变量int c;这个变量分配在栈中,我们可以简单的理解为,在声明临时变量c后,我们就向当前的程序栈中压入了一个int值:

    int c = a; <==> push(a);   //简单粗暴,临时变量的声明理解为简单地向栈中push一个值。
    

    那现在这个函数swap调用结束了,我们是否需要退栈,把之前临时变量c使用的栈空间返还回去?需要吗?不需要吗?
    我们假设不需要,当我们频繁调用swap的时候,会发生什么?每次调用,程序栈都在生长。直到栈满,我们就会收到stack overflow错误,程序挂掉了。
    所以为了避免这种乌龙的事情发生,我们需要在函数调用结束后,退栈,把堆栈还原到函数调用前的状态,这些被pop掉的临时变量,自然也就失效了,这也解释了我们一直以来关于临时变量仅在当前函数内有效的认知。其实堆栈平衡这个概念本身比这种粗浅的理解要复杂的多,还应包括压栈参数的平衡,暂时我们可以简单地这样理解,后面再做详细说明。

    2.3. 函数的参数传递和调用约定

    函数的参数传递是一个参数压栈的过程。函数的所有参数,都会依次被push到栈中。那调用约定有是什么呢?
    C和C++程序员应该对所谓的调用约定有一定的印象,就像下面这种代码:

    void __stdcall add(int a,int b);
    

    函数声明中的__stdcall就是关于调用约定的声明。其中标准C函数的默认调用约定是__stdcall,C++全局函数和静态成员函数的默认调用约定是__cdecl,类的成员函数的调用约定是__thiscall。剩下的还有__fastcall,__naked等。
    为什么要用所谓的调用约定?调用约定其实是一种约定方式,它指明了函数调用中的参数传递方式和堆栈平衡方式。

    2.3.1 参数传递方式

    还是之前那个例子,swap函数有2个参数,int a,int b。这两个参数,入栈的顺序谁先谁后?
    其实是从左到右入栈还是从右到左入栈都可以,只要函数调用者和函数内部使用相同的顺序存取参数即可。在上述的所有调用约定中,参数总是从右到左压栈,也就是最后一个参数先入栈。我们可以使用一份伪代码描述这个过程

    push b;      //先压入参数b
    push a;      //再压入参数a
    call swap;  //调用swap函数
    

    其实从这里我们就可以理解为什么在函数内部,不能改变函数外部参数的值:因为函数内部访问到的参数其实是压入栈的变量值,对它的修改只是修改了栈中的"副本"。指针和引用参数才能真正地改变外部变量的值。

    2.3.2 堆栈平衡方式

    因为函数调用过程中,参数需要压栈,所以在函数调用结束后,用于函数调用的压栈参数也需要退栈。那这个工作是交给调用者完成,还是在函数内部自己完成?其实两种都可以。调用者负责平衡堆栈的主要好处是可以实现可变参数(关于可变参数的话题,在此不做过多讨论。如果可能的话,我们可以以一篇单独的文章来讲这个问题),因为在参数可变的情况下,只有调用者才知道具体的压栈参数有几个。
    下面列出了常见调用约定的堆栈平衡方式:

    调用约定堆栈平衡方式
    __stdcall函数自己平衡
    __cdecl调用者负责平衡
    __thiscall调用者负责平衡
    __fastcall调用者负责平衡
    __naked编译器不负责平衡,由编写者自己负责

    2.4. 栈帧的概念:从esp和ebp说起

    为什么我们需要ebp和esp2个寄存器来访问栈?这种观念其实来自于函数的层级调用:函数A调用函数B,函数B调用函数C,函数C调用函数D…
    这种调用可能会涉及非常多的层次。编译器需要保证在这种复杂的嵌套调用中,能够正确地处理每个函数调用的堆栈平衡。所以我们引入了2个寄存器:

    1. ebp指向了本次函数调用开始时的栈顶指针,它也是本次函数调用时的“栈底”(这里的意思是,在一次函数调用中,ebp向下是函数的临时变量使用的空间)。在函数调用开始时,我们会使用把当前的esp保存在ebp中。
    mov ebp,esp 
    
    1. esp,它指向当前的栈顶,它是动态变化的,随着我们申请更多的临时变量,esp值不断减小(正如前文所说,栈是向下生长的)。函数调用结束,我们使用来还原之前保存的esp。
    mov esp,ebp
    

    在函数调用过程中,ebp和esp之间的空间被称为本次函数调用的“栈帧”。函数调用结束后,处于栈帧之前的所有内容都是本次函数调用过程中分配的临时变量,都需要被“返还”。这样在概念上,给了函数调用一个更明显的分界。下图是一个程序运行的某一时刻的栈帧图:

    Paste_Image.png

    3. 汇编中关于“函数调用”的实现

    上面铺陈了很多的汇编层面的概念后,我们终于可以切回到我们本次的主题:函数调用。
    函数调用其实可以看做4个过程,也就是本篇标题:

    1. 压栈: 函数参数压栈,返回地址压栈
    2. 跳转: 跳转到函数所在代码处执行
    3. 执行: 执行函数代码
    4. 返回: 平衡堆栈,找出之前的返回地址,跳转回之前的调用点之后,完成函数调用

    3.1 call指令 压栈和跳转

    下面我们看一下函数调用指令

    0x210000 call swap;
    0x210005 mov ecx,eax; 
    

    我们可以把它理解为2个指令:

    push 0x210005;
    jmp swap;
    

    也就是,首先把call指令的下一条指令地址作为本次函数调用的返回地址压栈,然后使用jmp指令修改指令指针寄存器EIP,使cpu执行swap函数的指令代码。

    3.2 ret指令 返回

    汇编中有ret相关的指令,它表示取出当前栈顶值,作为返回地址,并将指令指针寄存器EIP修改为该值,实现函数返回。
    下面给出一组示意图来演示函数的返回过程:

    1. 当前EIP的值为0x210004,指向指令ret 4,程序需要返回

    Paste_Image.png

    1. 执行ret指令,将当前esp指向的堆栈值当做返回地址,设置eip跳转到此处并弹出该值

    Paste_Image.png

    经过这两步,函数就返回到了调用处。

    4. 从实际汇编代码看函数调用

    4.1 程序源码和运行结果

    源码:

    main.cpp
    
    #include <stdio.h>
     
    void __stdcall swap(int& a, int& b);
     
    int main(int argc, char* argv)
    {
        int a = 1, b = 2;
        printf("before swap: a = %d, b = %d\r\n", a, b);
        swap(a, b);
        printf("after swap: a = %d, b = %d\r\n", a, b);
    }
     
     
    void __stdcall swap(int& a, int& b)
    {
        int c = a;
        a = b;
        b = c;
    }
    

    程序运行结果:

    Paste_Image.png

    4.2 反汇编

    Paste_Image.png

    Paste_Image.png

    可以看到,在函数调用前,函数参数已被压栈,此时:
    EBP = 00AFFCAC
    ESP = 00AFFBBC
    EIP = 00BF1853

    我们按F11,进入函数内部,此时:

    Paste_Image.png

    其实就是call swap指令的下一条指令地址,它就是本次函数调用的返回地址。

    Paste_Image.png

    下面是一个swap函数的详细注释:

    Paste_Image.png

    当程序运行到ret 8时

    Paste_Image.png

    执行返回后:

    Paste_Image.png

    在返回前,ESP = 00AFFBB8,返回后 ESP = 00AFFBC4
    0x00AFFBC4 - 0x00AFFBB8 = 0xC
    这里的数值是字节数,而我们知道,int是4字节长度。所以0xC/4 = 3
    正好是2个压栈参数+一个返回地址。

    4.3 调用堆栈

    调试程序的时候,我们经常关注的一个点就是VisualStudio显示给我们的“调用堆栈”功能,这次让我们来仔细看一下它:
    我们重新执行一次程序,这次我们关注一下vs显示的调用堆栈,如下图

    Paste_Image.png

    第一行是当前指令地址
    第二行是外层调用者,我们双击它,跳转到如下地址:

    Paste_Image.png

    也许这也是为什么这个功能被叫做“调用堆栈”的原因:它正是通过对程序栈的分析实现的。

    转于:https://www.jianshu.com/p/594357dff57e

    展开全文
  • 我们一般将字符串、列表等变量作为参数进行函数调用。但函数本身也是一个对象,所以我们也可以将函数作为参数传入另外一个函数中并进行调用。 本关的目标是让读者了解并掌握函数作为参数传入另外一个函数中并进行...
  • Shell中的函数调用

    万次阅读 2016-09-03 23:31:46
     ----------函数介绍  ----------函数定义  ----------函数使用  ----------区分return和exit  ----------删除函数  ----------注意事项   1、介绍函数 
  • javascript中函数的循环调用

    千次阅读 2017-09-16 17:43:14
    javascript中函数的循环调用
  • C语言函数调用时候内存中栈的动态变化详细分析

    千次阅读 多人点赞 2019-05-02 00:23:41
    先了解如下几点知识和过程: ...这些指令代码是存放在内存中进程的代码段,同一个函数内的指令代码是按照地址顺序存储的(编译器决定的)(也就是说只要指令地址+1就可以自动得到下一条指令的地址...
  • 复制的别人的代码(下面的代码),但Matlab总是在报错误——检查对函数‘Link’的调用中是否缺失参数或参数数据类型不正确。 L1 = Link([0 0 0 -pi/2 0],'standard'); L2 = Link([0 0.19 0.650 0 0],'standard'); L3
  • 简要介绍下函数名字修饰规则和函数调用约定的作用: 函数调用方式决定了函数参数入栈的顺序,是由调用者函数还是被调用函数负责清除栈中的参数等问题, 函数名修饰规则决定了编译器使用何种名字修饰方式来区分不同的...
  • 函数调用时的形参与实参

    万次阅读 多人点赞 2018-04-15 18:31:11
    大多数人在进行学习编程时,对于函数调用时函数时的传递的内容是一个头大的问题。本人在学习后有了以下的见解请大家共同学习,若有失误,请指出! 基础知识 1.系统堆栈(即栈区域)和堆区域。 1.1、栈(Stack)是...
  • 下面函数哪个是系统调用而不是库函数()? 正确答案: D 你的答案: D (正确) printf scanf fgetc read print_s scan_s 添加笔记 收藏 纠错 答:...
  • 写python程序的时候很多人习惯创建一个utils.py文件,存放一些经常使用的...或是两个文件之间的class或是函数调用情况。就像下面的工程目录一样: 工程目录 Project\ ... src\ main.py utils.py test.py ...
  • 绘制函数调用关系图对理解大型程序大有帮助。我想大家都有过一边读源码(并在头脑中维护一个调用栈),一边在纸上画函数调用关系,然后整理成图的经历。...下面举出我知道的几种免费的分析C/C++函数调用关系的工具...
  • 函数调用模式4种方式详解

    千次阅读 2017-03-15 22:17:55
    函数调用模式: 函数模式特征:就是一个简单的函数调用,函数名前面没有任何的引导内容function foo(){} var func = function(){}foo(); func(); (function(){})(); this在函数模式中的含义: this在函数中表示全
  • C语言中有关外部函数调用的问题

    万次阅读 多人点赞 2016-06-16 21:44:57
    对于外部函数实体(处于调用函数所在源文件之外的其他源文件中的函数),是在链接过程中,才会被寻找和添加进程序,一旦没有找到函数实体,就会报错,无法成功链接。 而外部函数的声明(一般声明在头文件中)只是令...
  • 本小节介绍通过缓冲区溢出调用函数,此外通过学习也对函数栈的了解有所加深,这里加上小段自己对函数栈的理解 函数栈的介绍 通过汇编介绍函数执行过程中栈的变化: EBP:栈底 存储着上一个栈的栈底EBP ESP:栈顶...
  • C++中构造函数调用其他函数

    千次阅读 2013-08-30 16:43:55
    另: 若构造函数调用自身,则会出现无限递归调用,是允许的 正确的方式: struct CLS { int m_i; CLS( int i ) : m_i(i){} CLS() { CLS(0); //new(this)CLS(0); //this->CLS::CLS(0); //CLS:...
  • Linux内核如何替换内核函数调用原始函数

    千次阅读 热门讨论 2018-11-18 17:16:42
    浙江温州皮鞋湿,下雨进水不会胖。周六的雨夜,期待明天的雨会更大更冷。 已经多久没有编程了?很久了吧…其实我本来就怎么会写代码,...替换一个内存中的函数,使得执行流流入我们自己的逻辑,然后再调用原始的函...
  • Python3 根据函数名动态调用函数

    千次阅读 2019-07-02 21:18:48
    最近项目需求,需要动态调用函数,这里整理一下思路。 很多时候,我们需要根据外面传进来的字符串取动态调用函数,并获取返回值。例如单项目检查等,检查的类型可以事先在定义文件中配置好,这样后期维护的时候,...
  • 在把VC6.0 工程转到VS2005工程中可能会遇到下面的问题 ...error C2668: “pow”: 对重载函数调用不明确 d:\program files\microsoft visual studio 8\vc\include\math.h(575): 可能是“long doubl
  • 同学们大家好,我是小伊同学,前面我们已经做好了小程序的页面,在开发好了后端的接口之后,我们就需要对前后端进行联通,而在云开发下,这部分内容就是我们今天将要一起学习的——小程序端如何调用函数。...
  • Linux内核模块间函数调用正确方法

    万次阅读 2015-03-15 16:39:39
    转载注明出处 : ...模块之间发生调用关系是常有的事情,下面以两个模块A、B,B使用A模块提供的函数为例,讲解正确使用的方法。模块A中使用EXPORT_SYMBOL或EXPORT_SYMBOL_GPL将要提供
  • 简述 C/C++ 的函数名修饰规则以及函数调用约定
  • 首先,函数调用是预料范围内的代码执行,是完全可控的,当前执行的函数调用另外一个函数时,是从当前代码段通过跳转指令主动跳转到另外一个代码段,只需保存跳转之前的栈顶指针(fp),栈底指针(sp)到栈空间,保存跳转...
  • 函数是 Python 程序的重要组成单位,一个 Python 程序可以由很多个函数组成。前面我们己经用过大量函数,如 len()、max() 等,使用函数是真正开始编程的第一步。 比如在程序中定义了一段代码,这段代码用于实现一...
  • 函数调用形式为函数名 (实参表列)。C语言可以嵌套调用函数。在调用函数时又调用该函数本身为递归,必须有退出条件。数组元素相当于普通变量,数组名作参数时要求形参和实参必须为类型相同的数组。局部变量只在本函数...
  • 一、函数调用模式 function add1(i){ console.log("函数声明:"+(i+1)); } add1(1);二、方法调用模式var myNumber = { value: 1, add: function(i){ console.log(this); this.value += i; } } myNumber.add...
  • gdb 如何调用函数

    千次阅读 2019-02-14 22:49:55
    我对 gdb 能够调用函数感到很吃惊。正如往常所做的那样,我在 Twitter 上询问这是如何工作的。我得到了大量的有用答案。我最喜欢的答案是 Evan Klitzke 的示例 C 代码,它展示了 gdb 如何调用函数。代...
  • Python学习--函数调用与声明问题

    万次阅读 2018-05-08 20:42:14
    发现一个奇怪的问题:Python在函数里面调用另一个函数时,被调用函数是不用事先定义的,例如现在这个例子是可以正确执行的:def test1(): test() def test(): print('test') test1() 这个例子中,函数test1()...

空空如也

空空如也

1 2 3 4 5 ... 20
收藏数 560,955
精华内容 224,382
关键字:

关于函数的调用下面不正确的是