0%

上篇文章 「Address Sanitizer 基本原理介绍及案例分析」里我们简单地介绍了一下 Address Sanitizer 基础的工作原理,这里我们再继续深挖一下深层次的原理。

从上篇文章中我们也了解到,对一个内存地址的操作:

1
2
*address = ...;  // 写操作
... = *address; // 读操作

当开启 Address Sanitizer 之后, 运行时库将会替换掉 mallocfree 函数,在 malloc 分配的内存区域前后设置“投毒”(poisoned)区域, 使用 free 释放之后的内存也会被隔离并投毒,poisoned 区域也被称为 redzone

上面的内存地址访问的代码,编译器会帮我们修改为这样的代码:

1
2
3
4
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;

这样对内存的访问,编译器会在编译期自动在所有内存访问之前通过判断 IsPoisoned(address) 做一下 check 是否被“投毒”。

那么实现且高效地实现 IsPoisoned(),并使得 ReportError() 函数比较紧凑就十分重要。

在深入了解之前,我们先了解 Shadow 内存,以及主应用内存区shadow 内存映射。

Read more »

Address Sanitizer 介绍

LLVM 提供了一系列的工具帮助 C/C++/Objc/Objc++ 开发者检查代码中可能的潜在问题,这些工具包括 Address Sanitizer,Memory Sanitizer,Thread Sanitizer,XRay 等等, 功能各异。

本篇主要介绍可能是最常用的一个工具 Address Sanitizer,它的主要作用是帮助开发者在运行时检测出内存地址访问的问题,比如访问了释放的内存,内存访问越界等。

全部种类如下,也都是非常常见的几类内存访问问题。

  1. Use after free
  2. Heap buffer overflow
  3. Stack buffer overflow
  4. Global buffer overflow
  5. Use after return
  6. Use after scope
  7. Initialization order bugs
  8. Memory leaks

这里为了便于理解,先介绍一下大概的工作原理。然后从上面几种场景中挑出几个有代表性的介绍一下。

Address Sanitizer 的基本工作原理

我们对一个内存地址的 访问 无外乎两种操作:,也就是

1
2
*address = ...;  // 写操作
... = *address; // 读操作

Address Sanitizer 的工作依赖编译器运行时库,当开启 Address Sanitizer 之后, 运行时库将会替换掉 mallocfree 函数,在 malloc 分配的内存区域前后设置“投毒”(poisoned)区域, 使用 free 释放之后的内存也会被隔离并投毒,poisoned 区域也被称为 redzone

这样对内存的访问,编译器会在编译期自动在所有内存访问之前做一下 check 是否被“投毒”。所以以上的代码,就会被编译器改成这样:

1
2
3
4
5
if (IsPoisoned(address)) {
ReportError(address, kAccessSize, kIsWrite);
}
*address = ...; // or: ... = *address;

Read more »

C++ 中使用 std::shared_ptr 智能指针不当有可能会造成循环引用,因为 std::shared_ptr 内部是基于引用计数来实现的, 当引用计数为 0 时,就会释放内部持有的裸指针。但是当 a 持有 b, b 也持有 a 时,相当于 a 和 b 的引用计数都至少为 1,因此得不到释放,RAII 此时也无能为力。这时就需要使用 weak_ptr 来打破循环引用。

通过 weak_ptr 来避免循环引用

来看一个比较典型的 delegate/observer 的场景:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#include <iostream>
#include <memory>

class DataFetcher {
public:
class Delegate {
public:
~Delegate() = default;
virtual void OnDataReady(void* any_data) = 0;
};

DataFetcher(std::weak_ptr<Delegate> delegate) : delegate_(delegate) {}

void FetchData() {
// ... fetch data from somewhere asynchronously
// and call back
auto delegate = delegate_.lock();
delegate->OnDataReady(nullptr);
}
private:
std::weak_ptr<Delegate> delegate_;
};

class DataManager : public DataFetcher::Delegate,
public std::enable_shared_from_this<DataManager> {
public:
DataManager() {}

void FetchData() {
if (!data_fetcher_) {
data_fetcher_ = std::make_shared<DataFetcher>(shared_from_this());
}
std::cout << "Will fetch data with data_fetcher_" << std::endl;
data_fetcher_->FetchData();
}
void OnDataReady(void* any_data) override {
std::cout << "Got Data!" << std::endl;
}

private:
std::shared_ptr<DataFetcher> data_fetcher_;
};


int main(int argc, char *argv[]) {
auto manager = std::make_shared<DataManager>();
manager->FetchData();
}

Read more »

本系列的第一篇 中介绍了 AArch64 的基础指令、进程内存布局以及基础栈操作 等. 本文该系列的第二篇, 主要聊聊函数调用, 涉及到的就是 Function Call Convention. 初衷还是尽可能 “浅入深出” 地 got 到语言背后的本质, 这不是一个手册, 所以不是完备的.

1. 我们在聊函数调用的时候在聊什么?

至少我们应该把函数调用的几个问题搞清楚:

  1. 函数在汇编层是怎么调用的, 本质是什么?
  2. 函数的参数怎么传?
  3. 返回值写到哪里? 怎么传给 caller?
  4. 调用完之后, 怎么返回到原来的位置?

Function Call Convention 其实就是回答这些问题的, 接下里我们一一找到答案.

1.1. 函数调用本质是什么?

汇编层是没有函数的概念的, 我们需要把函数映射到汇编层来, 这样我们就知道了它的本质. 其实执行一个程序, 在汇编层来看就是不断的执行 CPU 指令, 都执行完了, 进程就结束了. 从第一篇的例子其实可以看出, 一个函数就是一个label, 等于代码段中该函数第一条指令的位置. 其实本质上函数调用, 就是程序从代码段的某一条指令, 跳转到另外一个地址上的指令去执行. 稍微复杂点的 C 程序都不是从头执行到尾就结束了, 会有条件判断, 函数调用. 函数调用和普通跳转不同的地方在于要处理传参、返回、以及寄存器的 backup 和恢复.

AArch64 提供给我们了一个 bl (branch with link) 指令, 用来执行指定的函数. 第一篇里, 我们介绍了 cmp 以及 b.le/b.ge 等, ‘b’ 在这两处都是 branch 跳转的意思.

只不过 bl 是跳转的函数地址上, bl 内部实现是这样的:

  1. 跳转之前会把函数调用后面地址(也就是bl的下一条指令的地址) 存放到 LR (Link register) 中
  2. PC 被 bl 的参数替换, 就是 PC 指向了 bl 的参数, 通常是一个函数 label, 对应着一个地址
  3. 目标函数开始执行
  4. 目标函数执行完, 调用 ret 指令, ret 会把 LR copy 回 PC
  5. 程序执行 PC, 也就是执行原来 bl 下一条指令了
Read more »

1. 什么是 ARM?

正式开始之前, 我们先来了解一下什么是 ARM, 以及对应的一些概念.

Wikipedia 上是这么介绍 ARM 的:

ARM (stylised in lowercase as arm, formerly an acronym for Advanced RISC Machines and originally Acorn RISC Machine) is a family of reduced instruction set computer (RISC) instruction set architectures for computer processors, configured for various environments.

ARM 是 高级-RISC(精简指令集)-机器 的缩写, 是精简指令集架构的家族. 同时 Arm Ltd. 也是开发和设计、授权这项技术的公司名称.

1.1. 有哪些指令集架构呢? (TRDR, 可跳过)

目前用的比较多的架构是 ARMv7 和 ARMv8, 这两个名字各自都是一个系列.

在 ARMv7 以及之前都是最多支持 32 位架构(更早还有 16 位, 甚至更低), 那么 32 位架构对应的 ISA 也就是指令集称为 A32. 32 位下指令的地址空间最大只有 4GB, 苹果系列的代表是 iPhone 4 使用的 A4 芯片, 以及 iPhone 4s 使用的 A5 芯片.

2011 年面世的 ARMv8-A 架构增加了对 64 位地址空间的支持, 对应的 ISA 称为 A64. 这里用的词是“增加”, 也就意味着在支持 32 位的基础上增加了对 64 位的支持. 所以也可以看出来所谓的 32/64 位指的就是可寻址的最大地址空间. 苹果系列从 iPhone 5s 开始的 A7 芯片一直到 A15, 以及 Apple M1 系列开始都是基于 ARMv8.x-A 规范的.

那我们见到的 AArch64 是什么呢? 其实它和 AArch32 被称为 “执行状态” (execution state), 那么我们可以说 ARMv8-A 同时支持 AArch32 和 AArch64 两种状态, 在 AArch64 状态下, 运行的是 A64 指令集.

这里要注意 ARMv7/ARMv8-A、AArch32/AArch64 以及 A32/A64 在概念上的的区别, 但很多时候, 描述的范围都挺笼统的, 有些也是可以互相指代的, 大家知道就好.

上面说到指令集, 指令集是做什么用的呢? 我们为什么要了解这些?

指令集本质上定义了 CPU 提供的“接口”, 软件通过这些“接口”调用 CPU 硬件的能力来实现编程. 编译器在这里起到很关键的角色, 它把上层代码根据对应的架构, 编译为由该架构支持的指令集对应的二进制代码, 最终运行在 CPU 上.

对 C 系语言来说, 我们说的跨平台, 其实就是通过同一份源码在编译时, 根据不同 target 架构指令集, 生成不同的二进制文件来实现的.

Read more »

本系列的 第一篇 中介绍到了 AudioUnit 中和系统硬件交互的 IO Unit, 以及如何使用它进行音频的采集和播放. 本文是该系列的第二篇, 将会介绍 AudioUnit 中另外 四类 非常重要的 AudioUnit: MixingEffect UnitConverter Unit 以及 Generator Unit.

1. Mixing Unit

Mixing unit 在实际场景中非常的实用, 特别我们需要对多路音频做处理或者播放. 比如对于音频制作 app 来做, 通常要支持混入 N 多种乐器的声音和片段, 比如 吉他、钢琴、贝斯、人声、和声等等. 这个时候使用 Mixing unit 把这些 input bus 混成一路 output 交给 IO Unit 播放, 就是一个很必要且自然的结果.

Mixing Unit 是一个种类, 苹果内部提供了三个子类型:

1
2
3
4
5
CF_ENUM(UInt32) {
kAudioUnitSubType_MultiChannelMixing = 'mcmx',
kAudioUnitSubType_MatrixMixing = 'mxmx',
kAudioUnitSubType_SpatialMixing = '3dem',
};

1.1 MultiChannelMixing

MultiChannelMixing 是一个 多输入、单输出 的结构, 特点是:

  1. 支持任意多的 input bus, 每个 input 都可以有任意多的 channel(声道) 数
  2. 只有一路输出 ouput bus, 这一路 output bus 也可以有任意多的 channel 数

把这些 input bus 的声音混和, 从 output bus 输出, 每一路 input bus 可以独立设置数据源、音频格式、音量、mute 等, 这个是 Mixing 通用特点, 下同.

下面我们看一下它的结构, 相比 IO Unit 这个理解起来就比较简单了.

MixingNode

MultilChannelMixing 是 多输入、单输出 的结构, 可以自由配置 input bus 的数量, 配置完之后, 一个配置为 N 输入的 bus number 从 0 开始 到 N-1. Output bus 的个数只有一个, bus number 固定为 0.

每个 input bus 可以设置独立的 RenderCallback 或者连接前序的 AudioUnit 提供数据, 可以设置独立的音频格式参数, 以及控制当前 input 的音量和 mute 状态等等.

Read more »

Apple 平台上如果涉及到音频采集, 很难避开 AudioUnit 这个工具库, AudioUnit 是 Audio Toolbox 下的一套有年头的 C API, 功能相对也比较强大, 虽然苹果最近几年推出并逐渐在其基础之后完善了一套 AVAudioUnit 的 OC/Swift 的 API, 但 AudioUnit 依然有很广泛的使用, 而且了解这套 C API 也对理解 AVAudioUnit 内部的实现和使用有很大的帮助.

其实里面的概念并不是特别复杂, 但是因为文档比较老旧, 概念也比较绕, 上手并不易. 我此前做唱歌和直播 app 相关的工作, 对 AudioUnit 使用的也比较多, 积累了一些经验, 希望能够最大程度地把一些通用的概念和使用方法分享出来. 接下来将带大家剖析 AudioUnit 的内部原理和丰富多样的使用方式, 如果你在做涉及到声音采集和处理的工作, 希望能带大家深入浅出地摸透 AudioUnit.

关于 AudioUnit 的文章是一个系列, 我希望能够把之前的经验结合一些实际的场景来介绍, 大概分为一下四个部分:

  1. 熟悉 IO Unit 结构和运行机制, 使用它来进行录制和播放
  2. 熟悉其他类型的 AudioUnit, 比如 Mixer, Effect, Converter 等
  3. 使用 AUGraph 串联起来 AudioUnit, 以及常用的使用模式
  4. 熟悉使用 AVAudioUnit 进行音频采集和播放

本文中我们先来看第一部分.

1. AudioUnit 介绍

如下图, 可见 iOS 上所有的音频基础都是基于 AudioUnit 的, 比如 AudioToolbox、Media Player, AV Foundation 等都是在 AudioUnit 上做的封装. AudioUnit 本身处理效率非常高, 实时性也很强, 支持 VoIP 常见下进行回声消除、降噪等处理.

Image

Read more »

1. 函数指针

函数指针 (Function Pointer) 就是指向函数地址的指针

1
2
3
4
5
6
7
8
9
10
int Sum(int a, int b) {
return a + b;
}

typedef int(*SumFunc)(int x, int y);

// --------

SumFunc sum = Sum;
std::cout << sum(1, 2) << std::endl;

2. 函数对象

函数对象,也就是 Function Object, 也被称为 Functor,它可以被当作一个函数来调用。通常指重载了 operator() 的类对象。因为它是一个对象,因此它的优势是可以保存一些状态,比如下面的 padding 属性. 不过相对函数指针,多增加了一个类的实现,二进制体积也相应地增加。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class SumClass {
public:
SumClass(int padding): padding(padding){}

int operator()(int a, int b) {
return a + b + padding;
}

private:
int padding;
};

// ---------------------

SumClass sumObj(3);
std::cout << sumObj(1, 2) << std::endl;
// 等价于
std::cout << sumObj.operator()(1, 2) << std::endl;
Read more »

低延迟编码对于很多视频app来说都很重要,特别是对实时音视频场景。苹果在 WWDC 2021 在 VideoToolbox 里推出了一种新的低延迟编码模式。低延迟编码模式的主要目的是为实时通讯场景优化现有的编码流程。

低延迟视频编码有以下的特点,从而对一个实时视频通讯app进行优化。

  1. 处理效率高,最小化端到端的延迟
  2. 新增两种 profile: CBP & CHP,增强互操作性
  3. 引入时域伸缩编码(Temporal Scalability),当会话中有多个参与者的时候,提供高效的编码流程
  4. 支持设置最大帧量化参数(Max Frame QP),展示最好的视频质量
  5. 引入长期参考帧 LTR,提供一个可靠的机制从网络丢包错误中恢复通讯

1.低延迟视频编码一览

下图是苹果平台上视频编码管线的简图:

Image

Read more »