城市站点
> cdecl(面试常见问题之C++篇答案)
详细内容

cdecl(面试常见问题之C++篇答案)

时间:2022-08-16 00:48:09     人气:217     来源:www.zhongshaninfo.com     作者:爱发信息
概述:......
编译和调试C/C++程序编译过程
  • C/C++程序编译过程就是把C/C++代码百年成可执行文件的过程, 该过程分为4步
  • 预处理阶段
  1. 进行宏展开和宏替换
  2. 处理条件编译指令, 如#ifdef, #endif等
  3. 去掉注释
  4. 添加行号和文件名标识
  5. 保留#pargma编译器指令(#Pragma命令将设定编译器的状态或者是指示编译器完成一些特定的动作)
  • 编译阶段
  1. 编译程序所要作的工作就是通过词法分析, 语法和语义分析,在确认所有的指令都符合语法规则之后,将其翻译成等价的中间代码表示或汇编代码。
  2. 代码优化
  3. 重点关注函数压栈方式的编译处理__cdecl是C DECLaration的缩写, 表示C语言默认的函数调用方法: 所有参数从右到左依次入栈. 这些参数由调用者清除,称为手动清栈. 被调用函数不需要求调用者传递多少参数, 调用者传递过多或者过少的参数. 甚至完全不同的参数都不会产生编译阶段的错误。_stdcall 是StandardCall的缩写, 是C++的标准调用方式:所有参数从右到左依次入栈,如果是调用类成员的话, 最后一个入栈的是this指针。这些堆栈中的参数由被调用的函数在返回后清除, 使用的指令是 retnX,X表示参数占用的字节数. CPU在ret之后自动弹出X个字节的堆栈空间。称为自动清栈. 函数在编译的时候就必须确定参数个数. 并且调用者必须严格地控制参数的生成,不能多, 不能少, 否则返回后会出错。
  • 汇编阶段
  1. 汇编过程实际上指把汇编语言代码翻译成目标机器指令的过程
  2. 对于被翻译系统处理的每一个C语言源程序, 都将最终经过这一处理而得到相应的目标文件。目标文件中所存放的也就是与源程序等效的目标的机器语言代码。
  3. 目标文件由段组成. 通常一个目标文件中至少有两个段: 代码段和数据段
  • 链接阶段
  1. 将所有的目标文件代码拼接且重定位符号地址, 生成可执行文件
  2. 两种链接方式: 动态链接和静态链接
动态链接和静态链接区别
  • 静态链接
  1. 在编译时静态库和程序链接
  2. 一般命名为:libxxx.a
  3. 运行时,可执行目标文件已经装载完毕,速度快
  4. 但是多个程序需要静态库时每个都会复制一份,造成内存浪费
  5. 更新后需要重新编译
  • 动态链接
  1. 在运行时链接
  2. 一般命名: libxxx.so
  3. 运行时加载链接, 速度相对慢
  4. 运行时多个程序共享同一份动态库, 不会造成内存浪费
  5. 易更新, 无需重新编译
内存管理new/delete和malloc/free区别
  • new是C++关键字,需要编译器支持. 而malloc是C语言库函数
  • new失败时, 会抛出bad_alloc异常. malloc会返回NULL
  • new执行时, 先分配内存, 再调用类的构造函数初始化, malloc只会分配内存
  • new无需指定分配内存大小, 编译器会根据类型信息自行计算, malloc需要在参数里指出分配内存的大小
  • new成功会直接返回所分配的对象的指针, 而malloc只会返回void指针, 需要转化才能得到想要的对象的指针
  • C++允许重载new/delete操作符, 而malloc不允许重载, new从自由存储区上为对象动态分配内存空间, malloc从堆上分配内存
C++中有几种类型的new
  • plain new: 普通new, 特点在new和malloc区别里说了
  • nothrow new: 在空间分配失败时不会抛出异常, 而是返回NULL. 使用:

  • new operator: 只做两件事:(1)调用operator new (2)调用类的构造函数
  • http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    operator new: 可以重载, 实际以标准C malloc()完成
  • placement new: 在一块已经分配成功的内存上重新构造对象或对象数组. 不会出现内存分配失败, 因为他只调用构造函数而不会分配内存. 用placement new构造和对象数组, 要显式调用它们的析构函数来销毁, 而不要用delete[], 因为构造起来的对象或数组大小不一定等于原来内存的大小, 用delete会造成内存泄漏或运行时出现错误. 使用:

malloc原理

  • Malloc一般是从heap分配. heap会事先分配n个页, 放在一起, 并在里面做些标记表明哪些块是使用中的哪些是空闲的. malloc只是从这里面找到一个空闲的块。
  • 当开辟的空间小于 128K 时, 调用 brk()函数
  • 当开辟的空间大于 128K 时,调用mmap()系统调用函数来在虚拟地址空间中(堆和栈中间,称为"文件映射区域"的地方)找一块空间来开辟
C++内存分区
  • 自高地址到低地址依次是栈, 对, 全局数据区, 常量区和代码区
  • 栈是程序的局部变量存储区域, 分配效率高, 但是内存容量有限, 同时离开变量作用域后会存储单元会自动释放
  • 堆是程序由malloc分配的内存块, 需要手动释放内存空间
  • 自由存储区: new操作符从自由存储区(free store)上为对象动态分配内存空间,自由存储区是C++基于new操作符的一个抽象概念,凡是通过new操作符进行内

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    存申请,该内存即为自由存储区。自由存储区通常是堆
  • 全局/静态存储区: 全局变量和静态变量存储的区域, 未初始化的静态变量会自动初始化为0
  • 代码区: 存放函数体的二进制代码
面向对象面向对象编程的三大特征
  • 封装 继承 多态
纯虚函数和虚函数
  • 纯虚函数在定义类不能被实现, 只有继承类才能被实现. 可以认为是一种接口函数含有纯虚函数的类被称为抽象类
  • 虚函数是为了重载和多态需要, 在基类中可以实现, 子类重写后会覆盖基类的虚函数
构造函数中能调用其他成员函数吗
  • 可以, 但是不能调用类的静态成员函数, 虚函数以及以来类构造完成的函数
构造函数和析构函数中能用虚函数吗
  • 都不能
  • 在构造函数中: 父类对象会在子类之前进行构造, 此时子类部分数据成员还没初始化, 调用子类的虚函数时不安全的
  • 析构函数: 构函数是用来销毁一个对象的,在销毁一个对象时,先调用子类的析构函数,然后再调用基类的析构函数。所以在调用基类的析构函数时,派生类对象的数据成员已经“销毁”,这个时再调用子类的虚函数已经没有意义了。
构造函数和析构函数能成为虚函数吗
  • (1)构造一个对象的时候,必须知道对象的实际类型,而虚函数行为是在运行期间确定实际类型的. 而在构造一个对象时, 由于对象还未构造成功. 编译器无法知道对象的实际类型, 是该类本身, 还是该类的一个派生类. (2)虚函数的执行依赖于虚函数表. 而虚函数表在构造函数中进行初始化工作,即初始化vptr, 让他指向正确的虚函数表. 而在构造对象期间, 虚函数表还没有被初 始化,将无法进行。
  • 有虚函数的类的析构函数必须为虚函数. 因为只有序析构函数才能自动调用基类的析构函数.
重载, 覆盖和隐藏区别
  • 函数重载: 同一作用域内内, 一组具有不同参数列表的同名函数(静态绑定)
  1. C++编译器对函数名的处理: 作用域+返回类型+函数名+参数列表
  2. 参数列表必须不同(个数, 类型或参数排列顺序)
  3. 仅仅返回值不同不足以称为函数重载
  4. C语言不支持函数重载, 因为C语言在编译过程会保留原始的函数名
  • 覆盖: 重写指的是在派生类中覆盖基类中的同名函数,重写就是重写函数体要求基类函数必须是虚函数且函数参数列表必须相同(动态绑定)
  • 隐藏: 指的是某些情况下,派生类中的函数屏蔽了基类中的同名函数.
  1. 两个函数参数相同, 但是基类函数不是虚函数
  2. 两个函数参数不同, 无论基类函数是不是虚函数, 都会被隐藏
C++多态如何实现
  • 多态性: 在基类的函数前加上virtual关键字,在派生类中重写该函数,运行时将会根据所指对象的实际类型来调用相应的函数,如果对象类型是派生类,就调用派生类的函数,如果对象类型是基类,就调用基类的函数
  • 虚函数表: 在有虚函数的类中,类的最开始部分是一个虚函数表的指针vptr,这个指针指向一个虚函数表,表中放了虚函数的地址,实际的虚函数在代码段(.text)中。当子类继承了父类的时候也会继承其虚函数表,当子类重写父类中虚函数时候,会将其继承到的虚函数表中的地址替换为重新写的函数地址。使用了虚函数,会增加访问内存开销,降低效率。
  • 虚函数表结构(通过命令g++ -fdump-class-hierarchy xxx.cpp 查看):
  1. 非空虚基类

  1. 单继承

  1. 简单多继承1

  1. 简单多继承2

  1. 多重继承

  1. 虚继承

C++有几种构造函数

  • 拷贝构造函数
  • 默认构造函数
  • 移动构造函数
  • 委托构造函数
  • 继承构造函数
标准模板库list和vector区别
  • vector: 连续存储的容器,动态数组,在堆上分配空间
  1. 底层实现:数组
  2. 两倍容量增长
  3. 查询时间复杂度为O(1), 插入删除平均时间复杂度为O(n)
  • list: 在堆上分配空间,每插入一个元素都会分配空间,每删除一个元素都会释放空间
  1. 插入删除时间复杂度O(1), 查询时间复杂度O(n)
  2. 底层实现: 双向链表
迭代器失效的情况
  • set和map的操作不会使迭代器失效,因为删除只会导致被删除的元素的迭代器失效,因为被删除的节点不会影响其他节点
  • vector的删除会使后面的迭代器都失效
  • list的操作不会导致任何迭代器失效
vector扩容原理
  • vector是一个能够存放任意类型的动态数组
  • vector每次容量满了后会扩容, 一般为当前容量的两倍. 旧内存空间的内容按照原来顺序放到新的内存中
  • 最后将旧内存的内容释放掉, 但是存储空间没有释放
set和map区别
  • map和set都是C++的关联容器, 其底层实现都是红黑树(RB-Tree). 几乎所有的 map 和set的操作行为, 都只是转调 RB-tree 的操作行为。
  • map中的元素是key-value对, 而set只有value
  • set迭代器是const, 不允许修改元素的值. map允许修改value的值, 但是不允许修改key的值. 否则会使迭代器失效
  • map支持下标操作
set和unordered_set区别
  • set是有序的, 底层数据结构是红黑树
  • unordered_set是无序的,底层数据结构是哈希表
迭代器和指针区别
  • 迭代器不是指针, 各类模板, 表现得像指针. 他只是模拟了指针的一些功能, 通过重载了指针的一些操作符,->、*、++、--等.
  • 迭代器本质是封装了原生指针, 是指针概念的一种提升, 提供了比指针更高级的行为
  • 迭代器类的访问方式就是把不同集合类的访问逻辑抽象出来, 使得不用暴露集合内部的结构而达到循环遍历集合的效果
基础static作用
  • static变量默认初始化为0
  • static全局变量: 表示该变量作用域只在该变量所在的文件中. 在程序运行期间一直存在
  • static局部变量: 作用域仍为局部作用域, 但是离开局部作用后变量并没有销毁, 而是依然驻留在内存中, 下次该局部变量在进入作用域时, 值不变, static局部变量只会被初始化一次.
  • static函数: 函数的定义和声明在默认情况下都是extern的,但静态函数只是在声明他的文件当中可见,不能被其他文件所用。不要在头文件声明static函数
  • static成员变量: 静态成员可以实现多个对象之间的数据共享. 即无论对象有几个, 一个类中的static成员变量只有一个
  • static成员函数: 静态成员函数和静态数据成员一样,它们都属于类的静态成员,它们都不是对象成员. 没有this指针, 所以只能用类的静态成员变量和静态成员函数
指针和引用区别
  • 指针有自己的一块空间. 引用只是个别名
  • sizeof(指针)=4/8字节, 取决于机器位数. sizeof(引用)=引用对象大小
  • 指针可以被初始化为nullptr, 引用必须被初始化为一个已有对象的引用
  • 可以有const指针, 没有const引用
  • 指针可以有多级(**p), 引用只有一级
  • 指针和引用使用++运算符意义不同, 前者
  • 返回东爱内存分配的对象, 必须使用指针, 否则可能引起内存泄漏
  • 指针可以任意改变指向对象, 引用一旦被初始化就不能改变指向指针
  • 指针使用时需要解引用(->), 引用可以直接使用
结构体对齐
  • 第一个成员在结构体变量偏移量为0 的地址处,也就是第一个成员必须从头开始。
  • 其他成员变量要对齐到某个数字(对齐数)的整数倍的地址处。对齐数 为编译器默认的一个对齐数与该成员大小中的较小值。vs中默认值是8 Linux默认值为4(当然可以通过#pragma pack()修改),但修改只能设置成1,2,4,8,16.
  • 结构体总大小为最大对齐数的整数倍。(每个成员变量都有自己的对齐数)
  • 如果嵌套结构体,嵌套的结构体对齐到自己的最大对齐数的整数倍处,结构体的整体大小就是所有最大对齐数(包含嵌套结构体的对齐数)的整数倍。
区别一下指针类型

什么情况必须使用初始化成员列表

  • 初始化const成员
  • 初始化reference成员
  • 调用基类的构造函数
  • 调用数据成员对象的构造函数
strlen和sizeof区别
  • sizeof是运算符, 而不是函数, 结果在编译时得到. strlen是C语言库函数
  • sizeof参数可以是任何数据类型, strlen参数只能是字符指针且结尾是'0'的字符串
  • sizeof不能得到动态分配的变量的大小
常量指针和指针常量区别
  • 常量指针是指向常量的指针: const char *str="zhanyi";
  • 指针常量是指不能改变指向的指针: char *const str=p;
数组名和指针区别
  • sizeof得到结果不同
  • 字符数组内容可以改版, 字符指针内容不能改变
野指针和悬空指针
  • 野指针: 指针指向了被delete/free的内存
  • 空指针: 没有指向的指针
C和C++区别
  • C++是面向对象语言, C是面向过程语言
  • C++具有封装, 继承和多态三种特性
  • C++相比C, 增加许多类型安全的功能
  • C++支持方式编程, 不如模板函数和模板类
extern"C"用法
  • *
内联函数和宏定义区别
  • 根本区别宏定义只是字符替换, 内联函数是个函数(类型安全检查, 自动类型转换)
  • 代码展开发生在程序执行的不同阶段. 前者在预处理阶段, 后者在编译阶段
  • 内陆函数可以作为类的成员函数, 宏定义不能
const和指针
  • 顶层const: const修饰的变量本身是个常量, 即const在*右边
  • 底层const: const修饰的变量指向的对象是个常量, const在*左边
  • const_cast只对底层const起作用
define和const区别
  • define是在预处理阶段起作用, const在编译, 运行阶段起作用
  • define只做替换, 不做类型安全检查和计算, const相反
  • define在内存中有多份相同的备份, const只有一份
#和##在define中的作用assert其他C和C++的类型安全
  • 类型安全很大程度上可以等价于内存安全,类型安全的代码不会试图访问自己没被授权的内存区域
  • C的类型安全: C只在局部上下文中表现出类型安全, 比如试图从一种结构体的指针转换成另一种结构体的指针时, 编译器将会报告错误, 除非使用显式类型转换. 然而, C中相当多的操作是不安全的. 比如:

  • C++的类型安全: C++比C更有类型安全性.
  1. 操作符new返回的指针类型严格与对象匹配,而不是void*
  2. C中很多以void*为参数的函数可以改写为C++模板函数,而模板是支持类型检查的;
  3. 引入const关键字代替#define constants,它是有类型、有作用域的,而#define constants只是简单的文本替换
  4. 一些#define宏可被改写为inline函数,结合函数的重载,可在类型安全的前提下支持多种类型,当然改写为模板也能保证类型安全
  5. C++提供了dynamiccast关键字,使得转换过程更加安全,因为dynamiccast比static_cast涉及更多具体的类型检查。
volatile关键字
  • 提示编译器不要对访问该变量的代码进行优化
  • 两个作用:保证变量的内存可见性;禁止指令重新排序
  • 变量的内存可见性: 一个线程修改了某个变量, 对其他线程是可见的
explicit关键字
  • 只能用于修饰只有一个参数的类构造函数
  • 它的作用是表明该构造函数是显示的, 而非隐式的. 跟它相对应的另一个关键字是implicit, 意思是隐藏的, 类构造函数默认情况下即声明为implicit(隐式).
C++异常处理
  • 用throw抛出异常, 在try作用域中运行可能抛出异常的代码, 通过catch捕捉异常
内存泄漏
(声明: 网站所收集的部分公开资料来源于互联网,转载的目的在于传递更多信息及用于网络分享,并不代表本站赞同其观点和对其真实性负责,也不构成任何其他建议。本站部分作品是由网友自主投稿和发布、编辑整理上传,对此类作品本站仅提供交流平台,不为其版权负责。如果您发现网站上有侵犯您的知识产权的作品,请与我们取得联系,我们会及时修改或删除。 )
  • 来源:https://segmentfault.com/a/1190000038292644

    前言

    我们经常会讨论这样的问题:什么时候数据存储在堆栈 (Stack) 中,什么时候数据存储在堆 (Heap) 中。我们知道,局部变量是存储在堆栈中的;debug 时,查看堆栈可以知道函数的调用顺序;函数调用时传递参数,事实上是把参数压入堆栈,听起来,堆栈象一个大杂烩。那么,堆栈 (Stack) 到底是如何工作的呢?本文将详解 C/C++ 堆栈的工作机制。阅读时请注意以下几点:


    1)本文讨论的编译环境是 Visual C/C++,由于高级语言的堆栈工作机制大致相同,因此对其他编译环境或高级语言如 C# 也有意义。


    2)本文讨论的堆栈,是指程序为每个线程分配的默认堆栈,用以支持程序的运行,而不是指程序员为了实现算法而自己定义的堆栈。


    3) 本文讨论的平台为 intel x86。


    4)本文的主要部分将尽量避免涉及到汇编的知识,在本文最后可选章节,给出前面章节的反编译代码和注释。


    cdec

    5)结构化异常处理也是通过堆栈来实现的(当你使用 try…catch 语句时,使用的就是 c++ 对 windows 结构化异常处理的扩展),但是关于结构化异常处理的主题太复杂了,本文将不会涉及到。

    从一些基本的知识和概念开始

    1) 程序的堆栈是由处理器直接支持的。在 intel x86 的系统中,堆栈在内存中是从高地址向低地址扩展(这和自定义的堆栈从低地址向高地址扩展不同),如下图所示:


    因此,栈顶地址是不断减小的,越后入栈的数据,所处的地址也就越低。


    2) 在 32 位系统中,堆栈每个数据单元的大小为 4 字节。小于等于 4 字节的数据,比如字节、字、双字和布尔型,在堆栈中都是占 4 个字节的;大于 4 字节的数据在堆栈中占4字节整数倍的空间。


    3) 和堆栈的操作相关的两个寄存器是 EBP 寄存器和 ESP 寄存器的,本文中,你只需要把 EBP 和 ESP 理解成 2 个指针就可以了。ESP 寄存器总是指向堆栈的栈顶,执行 PUSH 命令向堆栈压入数据时,ESP减4,然后把数据拷贝到ESP指向的地址;执行POP 命令时,首先把 ESP 指向的数据拷贝到内存地址/寄存器中,然后 ESP 加 4。EBP 寄存器是用于访问堆栈中的数据的,它指向堆栈中间的某个位置(具体位置后文会具体讲解),函数的参数地址比 EBP 的值高,而函数的局部变量地址比 EBP 的值低,因此参数或局部变量总是通过 EBP 加减一定的偏移地址来访问的,比如,要访问函数的第一个参数为 EBP+8。


    4) 堆栈中到底存储了什么数据?包括了:函数的参数,函数的局部变量,寄存器的值(用以恢复寄存器),函数的返回地址以及用于结构化异常处理的数据(当函数中有 try…catch 语句时才有,本文不讨论)。这些数据是按照一定的顺序组织在一起的, 我们称之为一个堆栈帧(Stack Frame)。一个堆栈帧对应一次函数的调用。在函数开始时,对应的堆栈帧已经完整地建立了(所有的局部变量在函数帧建立时就已经分配好空间了,而不是随着函数的执行而不断创建和销毁的);在函数退出时,整个函数帧将被销毁。


    5) 在文中,我们把函数的调用者称为 caller(调用者),被调用的函数称为callee(被调用者)。之所以引入这个概念,是因为一个函数帧的建立和清理,有些工作是由 Caller 完成的,有些则是由 Callee 完成的。

    开始讨论堆栈是如何工作的

    我们来讨论堆栈的工作机制。堆栈是用来支持函数的调用和执行的,因此,我们下面将通过一组函数调用的例子来讲解,看下面的代码:

    这段代码本身并没有实际的意义,我们只是用它来跟踪堆栈。下面的章节我们来跟踪堆栈的建立,堆栈的使用和堆栈的销毁。

    堆栈的建立

    我们从main函数执行的第一行代码,即 int result=foo(3,4); 开始跟踪。这时 main 以及之前的函数对应的堆栈帧已经存在在堆栈中了,如下图所示:


    图1

    参数入栈

    当 foo 函数被调用,首先,caller(此时caller为main函数)把 foo 函数的两个参数:a=3,b=4 压入堆栈。参数入栈的顺序是由函数的调用约定 (Calling Convention) 决定的,我们将在后面一个专门的章节来讲解调用约定。一般来说,参数都是从右往左入栈的,因此,b=4 先压入堆栈,a=3 后压入,如图:


    返回地址入栈


    我们知道,当函数结束时,代码要返回到上一层函数继续执行,那么,函数如何知道该返回到哪个函数的什么位置执行呢?函数被调用时,会自动把下一条指令的地址压入堆栈,函数结束时,从堆栈读取这个地址,就可以跳转到该指令执行了。如果当前"call foo"指令的地址是 0x00171482 ,由于 call 指令占 5 个字节,那么下一个指令的地址为 0x00171487,0x00171487 将被压入堆栈:


    代码跳转到被调用函数执行


    返回地址入栈后,代码跳转到被调用函数 foo 中执行。到目前为止,堆栈帧的前一部分,是由 caller 构建的;而在此之后,堆栈帧的其他部分是由 callee 来构建。


    EBP指针入栈


    在 foo 函数中,首先将 EBP 寄存器的值压入堆栈。因为此时 EBP 寄存器的值还是用于 main 函数的,用来访问 main 函数的参数和局部变量的,因此需要将它暂存在堆栈中,在 foo 函数退出时恢复。同时,给 EBP 赋于新值。


    1)将 EBP 压入堆栈


    2)把 ESP 的值赋给 EBP


    图4


    这样一来,我们很容易发现当前EBP寄存器指向的堆栈地址就是 EBP 先前值的地址,你还会发现发现,EBP+4 的地址就是函数返回值的地址,EBP+8 就是函数的第一个参数的地址(第一个参数地址并不一定是 EBP+8,后文中将讲到)。因此,通过 EBP 很容易查找函数是被谁调用的或者访问函数的参数(或局部变量)。



    为局部变量分配地址

    接着,foo 函数将为局部变量分配地址。程序并不是将局部变量一个个压入堆栈的,而是将 ESP 减去某个值,直接为所有的局部变量分配空间,比如在 foo 函数中有 ESP=ESP-0x00E4,(根据烛秋兄在其他编译环境上的测试,也可能使用 push 命令分配地址,本质上并没有差别,特此说明)如图所示:


    图5


    奇怪的是,在 debug 模式下,编译器为局部变量分配的空间远远大于实际所需,而且局部变量之间的地址不是连续的(据我观察,总是间隔 8 个字节)如下图所示:



    图6


    我还不知道编译器为什么这么设计,或许是为了在堆栈中插入调试数据,不过这无碍我们今天的讨论。


    通用寄存器入栈


    最后,将函数中使用到的通用寄存器入栈,暂存起来,以便函数结束时恢复。在 foo 函数中用到的通用寄存器是 EBX,ESI,EDI,将它们压入堆栈,如图所示:


    图7


    至此,一个完整的堆栈帧建立起来了。


    堆栈特性分析


    上一节中,一个完整的堆栈帧已经建立起来,现在函数可以开始正式执行代码了。本节我们对堆栈的特性进行分析,有助于了解函数与堆栈帧的依赖关系。


    1)一个完整的堆栈帧建立起来后,在函数执行的整个生命周期中,它的结构和大小都是保持不变的;不论函数在什么时候被谁调用,它对应的堆栈帧的结构也是一定的。


    2)在 A 函数中调用B函数,对应的,是在A函数对应的堆栈帧“下方”建立 B 函数的堆栈帧。例如在 foo 函数中调用 foo1 函数,foo1 函数的堆栈帧将在 foo 函数的堆栈帧下方建立。如下图所示:


    图8


    3)函数用 EBP 寄存器来访问参数和局部变量。我们知道,参数的地址总是比 EBP 的值高,而局部变量的地址总是比 EBP 的值低。而在特定的堆栈帧中,每个参数或局部变量相对于 EBP 的地址偏移总是固定的。因此函数对参数和局部变量的的访问是通过 EBP 加上某个偏移量来访问的。比如,在 foo 函数中,EBP+8 为第一个参数的地址,EBP-8 为第一个局部变量的地址。


    4)如果仔细思考,我们很容易发现 EBP 寄存器还有一个非常重要的特性,请看下图中:


    图9


    我们发现,EBP 寄存器总是指向先前的 EBP,而先前的 EBP 又指向先前的先前的 EBP,这样就在堆栈中形成了一个链表!这个特性有什么用呢,我们知道 EBP+4 地址存储了函数的返回地址,通过该地址我们可以知道当前函数的上一级函数(通过在符号文件中查找距该函数返回地址最近的函数地址,该函数即当前函数的上一级函数),以此类推,我们就可以知道当前线程整个的函数调用顺序。事实上,调试器正是这么做的,这也就是为什么调试时我们查看函数调用顺序时总是说“查看堆栈”了。

    返回值是如何传递的

    堆栈帧建立起后,函数的代码真正地开始执行,它会操作堆栈中的参数,操作堆栈中的局部变量,甚至在堆(Heap)上创建对象,balabala….,终于函数完成了它的工作,有些函数需要将结果返回给它的上一层函数,这是怎么做的呢?


    首先,caller 和 callee 在这个问题上要有一个“约定”,由于 caller 是不知道 callee 内部是如何执行的,因此 caller 需要从 callee 的函数声明就可以知道应该从什么地方取得返回值。同样的,callee 不能随便把返回值放在某个寄存器或者内存中而指望Caller 能够正确地获得的,它应该根据函数的声明,按照“约定”把返回值放在正确的”地方“。下面我们来讲解这个“约定”:


    1)首先,如果返回值等于 4 字节,函数将把返回值赋予EAX寄存器,通过 EAX 寄存器返回。例如返回值是字节、字、双字、布尔型、指针等类型,都通过 EAX 寄存器返回。


    2)如果返回值等于 8 字节,函数将把返回值赋予 EAX 和 EDX 寄存器,通过 EAX 和 EDX 寄存器返回,EDX 存储高位 4 字节,EAX存储低位 4 字节。例如返回值类型为 __int64 或者 8 字节的结构体通过 EAX 和 EDX 返回。


    3) 如果返回值为 double 或 float 型,函数将把返回值赋予浮点寄存器,通过浮点寄存器返回。


    4)如果返回值是一个大于 8 字节的数据,将如何传递返回值呢?这是一个比较麻烦的问题,我们将详细讲解:


    我们修改 foo 函数的定义如下并将它的代码做适当的修改:


    MyStruct定义为:


    这时,在调用 foo 函数时参数的入栈过程会有所不同,如下图所示:


    图10


    caller 会在压入最左边的参数后,再压入一个指针,我们姑且叫它ReturnValuePointer,ReturnValuePointer 指向 caller 局部变量区的一块未命名的地址,这块地址将用来存储 callee 的返回值。函数返回时,callee 把返回值拷贝到ReturnValuePointer 指向的地址中,然后把 ReturnValuePointer 的地址赋予 EAX 寄存器。函数返回后,caller 通过 EAX 寄存器找到 ReturnValuePointer,然后通过ReturnValuePointer 找到返回值,最后,caller 把返回值拷贝到负责接收的局部变量上(如果接收返回值的话)。


    你或许会有这样的疑问,函数返回后,对应的堆栈帧已经被销毁,而ReturnValuePointer 是在该堆栈帧中,不也应该被销毁了吗?对的,堆栈帧是被销毁了,但是程序不会自动清理其中的值,因此 ReturnValuePointer 中

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    的值还是有效的。

    堆栈帧的销毁

    当函数将返回值赋予某些寄存器或者拷贝到堆栈的某个地方后,函数开始清理堆栈帧,准备退出。堆栈帧的清理顺序和堆栈建立的顺序刚好相反:(堆栈帧的销毁过程就不一一画图说明了)


    1)如果有对象存储在堆栈帧中,对象的析构函数会被函数调用。


    2)从堆栈中弹出先前的通用寄存器的值,恢复通用寄存器。


    3)ESP 加上某个值,回收局部变量的地址空间(加上的值和堆栈帧建立时分配给局部变量的地址大小相同)。


    4)从堆栈中弹出先前的 EBP 寄存器的值,恢复 EBP 寄存器。


    5)从堆栈中弹出函数的返回地址,准备跳转到函数的返回地址处继续执行。


    6)ESP 加上某个值,回收所有的参数地址。


    前面 1-5 条都是由 callee 完成的。而第 6 条,参数地址的回收,是由 caller 或者callee 完成是由函数使用的调用约定(calling convention )来决定的。下面的小节我们就来讲解函数的调用约定。

    函数的调用约定(calling convention)

    函数的调用约定 (calling convention) 指的是进入函数时,函数的参数是以什么顺序压入堆栈的,函数退出时,又是由谁(Caller还是Callee)来清理堆栈中的参数。有 2 个办法可以指定函数使用的调用约定:


    1)在函数定义时加上修饰符来指

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    定,如



    2)在 VS 工程设置中为工程中定义的所有的函数指定默认的调用约定:在工程的主菜单打开 Project|Project Property|Configuration Properties|C/C++|Advanced|Calling Convention,选择调用约定(注意:这种做法对类成员函数无效)。


    常用的调用约定有以下3种:


    1)__cdecl。这是 VC 编译器默认的调用约定。其规则是:参数从右向左压入堆栈,函数退出时由 caller 清理堆栈中的参数。这种调用约定的特点是支持可变数量的参数,比如 printf 方法。由于 callee 不知道caller到底将多少参数压入堆栈,因此callee 就没有办法自己清理堆栈,所以只有函数退出之后,由 caller 清理堆栈,因为 caller 总是知道自己传入了多少参数。


    2)__stdcall。所有的 Windows API 都使用 __stdcall。其规则是:参数从右向左压入堆栈,函数退出时由 callee 自己清理堆栈中的参数。由于参数是由 callee 自己清理的,所以 __stdcall 不支持可变数量的参数。


    3) __thiscall。类成员函数默认使用的调用约定。其规则是:参数从右向左压入堆栈,x86 构架下 this 指针通过 ECX 寄存器传递,函数退出时由 callee 清理堆栈中的参数,x86构架下this指针通过ECX寄存器传递。同样不支持可变数量的参数。如果显式地把类成员函数声明为使用__cdecl或者__stdcall,那么,将采用__cdecl或者__stdcall的规则来压栈和出栈,而this指针将作为函数的第一个参数最后压入堆栈,而不是使用ECX寄存器来传递了。

    反编译代码的跟踪(不熟悉汇编可跳过)

    以下代码为和 foo 函数对应的堆栈帧建立相关的代码的反编译代码,我将逐行给出注释,可对照前文中对堆栈的描述:


    main 函数中 int result=foo(3,4); 的反汇编:


    下面是 foo 函数代码正式执行前和执行后的反汇编代码

  • 来源:经济日报-中国经济网

    近日,2019中国国际数字娱乐产业大会(CDEC)在上海开幕。这是数字娱乐产业最具专业性、权威性、国际性的顶级盛会。此次大会以“挑战 机遇 梦想”为主题,邀请了腾讯、网易、完美世界、盛趣游戏、万代南梦宫、IGG等海内外知名数字娱乐企业参加,在全面呈现新技术、政策、市场前景等数字娱乐产业发展前沿的同时,汇聚行业内最优质的资源共同探索新时代数字文创产业的转型之道,同时也让全球数字娱乐界共享中国产业生态发展的先机。

    赋能传统行业形成文创“新风口”

    cdec

    数字文创以文化创意内容为核心,依托数字技术进行创作、生产、传播和服务,能给用户带来更好的精神娱乐体验,从而建立起长期的情感连接。当这种“文创基因”赋能传统行业时,不仅有利于帮助传统行业重塑与用户的关系,推动

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    传统行业转型升级,也将拓宽数字文创产业的发展空间。

    目前,市场上传统行业与数字文创的融合常见的做法是在影视剧中做植入,或者依托互联网企业做APP等产品,以期带动传统产业和区域经济发展

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    ,但效果难达预期。当下传统行业与数字文创的合作多流于表面,由于没有为用户创造更多的价值,因此很难实现流量的变现和持久。更多时候因短期流量消耗了自身口碑,影响长期发展。

    新时代下,传统行业积极呼唤“文创基因”。如何才能实现“基因”的融合和重组,是传统行业和数字文创产业都要思考的问题。完美世界CEO萧泓指出,“传统行业需要构建属于自己的品牌IP矩阵、数字文创思维的人才体系,以及充分利用虚拟现实、5G等新科技带来的新一轮与用户建立更充分连接的发展浪潮。帮助传统行业实现这一升级,而这正是新经济时代下文创企业的创新与担当。”

    例如,完美世界作为国内领先的数字文创企业,旗下经典IP《诛仙》就与南京夫子庙、非遗文化龙泉刀剑以及敦煌文化合作,让用户们在游戏里感受深厚的中华文化;根据岩寺皖南龙的形象,研发出动漫IP《恐龙旅社》和“小恐龙”,不仅推动安徽地质博物馆成为地质类博物馆的标杆,还进行文具类等文创产品及系列衍生品制作,助力文博行业插上创意的翅膀。同时,基于对产业的深刻理解,完美世界正在构建完善的数字文创人才体系。完美世界控股集团旗下完美世界教育就以游戏化思维育人,通过游戏化思维与不同行业进行合作,不仅帮助想要进入游戏等数字文创领域的年轻人以更开阔的视野认识行业,同时也助力各行各业的人才通过游戏化思维的培养反哺其所在的行业。完美世界教育正在与海外大学探讨设立针对游戏行业的EMBA项目,培养需要游戏化思维来改造、推动传统行业升级的管理人才。

    文创“新秀”电竞未来可释放的空间巨大

    《2019年1-6月中国游戏产业报告》显示,2019年1-6月,中国电子竞技游戏市场实际销售收入465亿元,同比增长11.3%。电子竞技游戏市场近三年均保持了两位数较快

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    增长,体现了电子竞技行业在国内蓬勃发展的势头。电竞依托于技术升级和艺术表达方式上的推陈出新,可能衍生出众多新业态,围绕电竞IP,“电竞+”娱乐内容将大大丰富,电竞综艺、电竞实景体验、电竞电影、电竞音乐、电竞社交等也将“百花齐放”。目前,上海、成都、杭州、重庆、西安等城市纷纷布局电竞,以进一步拓宽城市发展空间,振兴区域经济发展。

    同时,电竞的载体形式决定了其会随着技术和时代的改变而不断变化,总有新鲜的体验给观众,还会成为新技术的应用场。随着互联网技术的发展,尤其是我国引领5G商用新时代的到来,电子竞技将有新一轮变革。萧泓认为,“5G的实现将彻底颠覆电竞行业。无论从供给端还是消费端来看,5G都会改变以往的娱乐形式,给用户带来一种新的竞技快感。像5G、VR等可以带来远程观赛和第一视角参与体验,AI技术可以赋能电竞选手的训练。”

    电竞发展迅速,但是产业人才匮乏,涉及整个产业链。人社部发布的《电子竞技员就业景气现状分析报告》显示,现阶段电竞产业从业者超过44万人,但仍有50余万人才缺口,其岗位覆盖游戏制作人、游戏解说员、职业教练、电竞选手等等。只有统筹赛事管理、赛事运营等各个环节,才能最大程度满足产业用人需求,提高城市核心竞争力。

    据了解,近年来,完美世界控股集团旗下完美世界教育就打造了一套科学、前沿的电竞专业人才培养模型,通过产教融合为教育端提供源自产业的教学解决方案。其与四川传媒学院共建国际游戏与电竞学院;与上海市信息管理学校、上海群星职业技术学校、上海电子信息职业技术学院等进行电竞教育校企合作;与泰国教育部合作,与泰国宣素那他皇家大学等高校共建电竞人才专业。这些合作平台汇聚专业师资,通过真实的项目案例将理论与实践相结合,不仅为学校提供前沿理念和专业技

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    术,也让学生能提前接触到电竞赛事全方位运营。另外,完美世界还将电竞人才的培养融入城市发展中,以真正具有数字文创思维的人才助推城市发展,比如为顺应上海浦东新区打造电竞

    http://www.jsyunjun.com/file/upload/tt1999/999.jpg

    产业发展核心功能区的目标,完美世界将在积极配合上海政府电竞策略的前提下,集聚产业上下游资源,进行阶梯式的系统人才培养和储备,帮助浦东夯实电竞产业基础,打造“电竞之都”。

    数字文创产业具有高融合性,在新经济时代下更是充满无限可能。作为市场前景十分广阔的新兴市场,如何进一步挖掘产业价值,无法一言以蔽之。赋能传统行业是新方向之一,挖掘电竞价值也是一种可行路径,除此之外,我们还将继续求索。

  • 阅读全文
    分享