cpp 中的函数

# Lecture06 - 函数

# 运行时环境 runtime environment

永远分配 full stay(?)-- 早期变量存储不分全局变量和局部变量,比较浪费空间,无法使用 recursive 递归

dynamic-- 函数调用完即刻回收,支持递归,节约空间

printf("%d",x);
//x 地址什么时候填入(符号表、link 时)

增加函数 function 需要在 stack 中付出代价 cost

stack 编译器帮助程序员做好

heap 中存储动态变量,编译器未知

Dynamic-link library DLL 动态链接库

在代码段中,以一个函数为基本单位

为什么 code、data、stack、heap 中不能 x(执行)?

攻击(断章取义)—— 利用看似正常的代码段。攻击操作系统。从头操作则没有问题。避免攻击,取消了执行。

# name mangling

c 中 func 不可以重载,但是 cpp 中函数可以重载(参数可以不同),func 不能够唯一指代。

cpp 中加了一些表示 --name mangling。

//cpp
int func(int d);//_Z4funci  
int func(int, double);//_Z4funcid

要告诉编译器

extern = "C" // 编译的是 c

# 原则和执行机制

# 原则

  1. 定义不允许嵌套(函数中不能有另外一个 definition)
  2. 先定义后使用

# 函数的执行机制

  1. 建立被调用函数的栈空间

  2. 参数传递

    • 值传递 call by value(c 中只有值传递,想要改变原值需要指针)
    • 引用传递 call by reference(cpp 中的隐式指针)int &a
  3. 保存调用函数的运行状态(回来之后还要接着执行)

  4. 将控制转交被调函数

EBP extended base pointer

ESP extended stack pointer

EIP is a register in x86 architectures (32bit). It holds the "Extended Instruction Pointer" for the stack. In other words, it tells the computer where to go next to execute the next command and controls the flow of a program.

stack 从 high 到 low 增长

# 如何调用函数 以及函数付出的代价

__cdecl

__stdcall

__fastcall

__thiscall

int func(int a, int b);
x = func(1,2);
// 从右往左传递参数

在函数中需要初始化变量,编译器在返回时,不会主动清理 stack 中的数据

# cost

  1. 付出指令的开销

  2. 付出栈的开销

# Summary

  1. 加载参数 (进栈)
  2. 保存上下文环境
    • 保存返回地址
    • 保存调用者基指针
  3. 执行函数
    • 设置新的基指针
    • 分配空间 (可选)
    • 执行一些任务
    • 释放空间 (如果分配了的话)
  4. 恢复上下文环境
    • 加载调用者基指针
    • 加载返回指针
  5. 继续执行调用者的功能 a

# 静态成员函数和非静态成员函数的差别

静态成员函数中没有 this,非静态成员函数有 this

静态成员函数形同于全局函数

# printf 和 cout

int printf ( const char * format, ... );

可变参数可被利用,进行攻击。

p
cout << "c" << endl;

cout 是个对象,cout <<"c" 是一个新的对象

# stdcall

调用者借还参数

参数未知长度

复用次数多,消耗空间和时间

# fastcall

寄存器有限,如果要用更多的寄存器,就要保存更多的数据(现场)

其实最多有两个参数 move 到寄存器,再有多的参数,就需要 push 了

# 函数原型

原型,只需要类型

  1. 遵守先定义后使用原则

  2. 自由安排函数定义位置

  3. 语句

  4. 只需参数类型,无需参数名称

  5. 编译器检查

  6. 函数原型:只需要看到函数名和参数读取到即可:

    int func(int,int)
    
    • 在调用点一定要能看到接口
    • 仅仅需要函数名和参数类型即可
  7. 函数原型应当放置在头文件中

# 函数重载

type conversion

# 原则

  1. 名同,参数不同(个数、类型、顺序)
  2. 返回值类型不作为区别重载函数的依据

# 匹配原则

Polymorphism 多态 -> 增强语言的丰富性和灵活性 -> 一名多用,并不只存在 OO programming 中

  1. 严格
  2. 内部转换
  3. 用户定义的转换

void f(long); void f(double);

f(10);

10 既可以是 int 也可以是 double,编译器会感到困惑,ambiguous

# 函数 - 默认参数

int func(int a = 1, int b = 2, int c = 3);

# 默认参数的声明

  1. 函数原型中给出 -> 是给使用点看的
  2. 先定义的函数中给出

# 默认参数的顺序

  1. 从右到左
  2. 不间断

# 默认参数与函数重载

void f(int); void f(int, int=2);

ambiguous

# 函数

Q1、能在不降低可读性的前提下,降低 COST 吗?

HL src ---compiler----》 machine language

程序员看到的是函数,compiler 把 block 移到调用处

问题:

  1. 假设 function 有 1000 行,代码拉长,存储占用率大
  2. 不再有递归

程序员主动提出:在 function 前写 inline

Q2、使用 lib 需要注意什么?

注意区分是哪种调用方式,哪种 compiler。

c 中不允许重载,cpp 可以

image-20221004190900029

# 内联函数 inline

  1. 目的:

    1. 提高可读性
    2. 提高效率
    3. 解决了两个 cost 的问题
  2. 对象: 使用频率高、简单、小段代码

    简单:没有 loop 或者 switch,不要有多个接口

    例如:OO 的 class 的 constructor 函数(构造函数)

  3. 实现方法:编译系统将为 inline 函数创建一段代码,在调用点,用相应的代码替换

  4. 限制:

    1. 递归 —— 必须是非递归函数,因为已经加入主体部分了

    2. 函数指针 —— 由编译系统控制,和编译器是完全相关的

      函数指针最主要的作用为写框架(framework)

  5. inline 关键字 仅仅是请求

    1. 有可能是递归,无法加入
    2. 也有可能是很复杂的函数,导致无法理解 (上下文比较复杂)
  6. 提请 inline 被拒绝时是有代价的

  7. 如果对象的初始化 - 构造函数为明确给出,计算机会给出 inline 的构造函数

  8. 宏: max(a,b) (a) > (b) ? (a) : (b) :不同于 inline 函数,一定要有括号,因为运算数据中的优先级不同

  9. 时间局部性:是指如果程序中的某条指令一旦执行,则不久之后该指令可能再次被执行;如果某数据被访问,则不久之后该数据可能再次被访问。 空间局部性是指一旦程序访问了某个存储单元,则不久之后。

    空间局部性:执行周边的代码的可能性很大,将附近的代码放到 cache 中,如果代码块过大,会造成抖动,cache 不断切换内存中的代码段。

image-20221004191015068

# 攻击

# stack

攻击:在 stack 中写入地址执行

解决:

  1. W 正交 X
  2. 禁止写入

# code

攻击:利用已有的代码进行攻击,比如利用 return 之前的语句

解决:

  1. R 正交 X
  2. 打乱顺序 load
  3. 检测,如果多次跳转 / 踩点,则可能是攻击的代码

image-20221004191213187

# ROP

# 定义

Return-oriented programming 返回导向编程,这是一种高级的内存攻击技术可以用来绕过现代操作系统的各种通用防御(比如内存不可执行和代码签名【代码签名是对可执行文件或脚本进行数字签名。用来确认软件的来源并保证在签名后未被修改或损坏的措施】等。

通过上一篇文章走进栈溢出,我们可以发现栈溢出的控制点是 ret 处,那么 ROP 的核心思想就是利用以 ret 结尾的指令序列把栈中的应该返回 EIP 的地址更改成我们需要的值,从而控制程序的执行流程。

ROP 方法技巧性很强,那它能完全胜任所有攻击吗?返回语句前的指令是否会因为功能单一,而无法实施预期的攻击目标呢?业界大牛已经过充分研究并证明 ROP 方法是图灵完备的,换句话说, ROP 可以借用 libc 的指令实现任何逻辑功能。