0%

性能指标

相位噪声、杂散以及瞬态响应

相位噪声

在频域中,这通常被认为是噪声功率相对于载波功率的密度

频谱分析仪测试相位噪声的一个示例。

img

杂散

杂散可以被认为是集中在与载波的特定偏移处的噪声。

杂散有很多种,它们可能有多种原因,但其中大多数发生在非常可预测的偏移处。 杂散倾向于出现在鉴频鉴相器频率、输入参考频率、通道间隔和几分之一通道间隔的倍数处。

img

阅读全文 »

MIPS 的协处理器 CP0 (Section 1)

在 MIPS 体系结构中,最多支持 4 个协处理器 (Co-Processor)。其中,协处理器 CP0 是体系结构中必须实现的。它起到控制 CPU 的作用。MMU、异常处理、乘除法等功能,都依赖于协处理器 CP0 来实现。它是 MIPS 的精髓之一,也是打开 MIPS 特权级模式的大门。
  MIPS 的 CP0 包含 32 个寄存器。关于它们的资料可以参照 MIPS 官方的资料 MIPS32(R) Architecture For Programmers Volume III: The MIPS32(R) Privileged Resource Architecture 的 Chap7 和 Chap8。本文中,仅讨论常见的一些寄存器。

  • Register 0: Index,作为 MMU 的索引用。将来讨论 MMU 和 TLB 时会详解之
  • Register 2, EntryLo0,访问 TLB Entry 偶数页中的地址低 32Bit 用。同上,在 MMU 和 TLB 的相关章节中详解。
  • Register 3, EntryLo1,访问 TLB Entry 奇数页中的地址低 32Bit 用。
  • Register 4, Context,用以加速 TLB Miss 异常的处理。
  • Register 5, PageMask,用以在 MMU 中分配可变大小的内存页。
  • Register 8, BadVAddr,在系统捕获到 TLB Miss 或 Address Error 这两种 Exception 时,发生错误地址会储存在该的虚拟寄存器中。对于引发 Exception 的 Bug 的定位来说,这个寄存器非常重要。
  • Register 9, Count,这个寄存器是 R4000 以后的 MIPS 系统引入的。它是一个计数器,计数频率是系统主频的 1/2。BCM1125/1250,RMI XLR 系列以及 Octeon 的 Cavium 处理器均支持该寄存器。对于操作系统来说,可以通过读取该寄存器的值来获取 tick 的时基。在系统性能测试中,利用该寄存器也可以实现打点计数。
  • Register 10,EntryHi,这个寄存器同 EntryLo0/1 一样,用于 MMU 中。以后会详述。
  • Register 11,Compare,配合 Count 使用。当 Compare 和 Count 的值相等的时候,会触发一个硬件中断 (Hardware Interrupt),并且总是使用 Cause 寄存器的 IP7 位。
  • Register 12,Status,用于处理器状态的控制。
  • Register 13,Cause,这个寄存器体现了处理器异常发生的原因。
  • Register 14,EPC,这个寄存器存放异常发生时,系统正在执行的指令的地址
  • Register 15,PRID,这个寄存器是只读的,标识处理器的版本信息。向其中写入无意义。
  • Register 18/19,WatchLo/WatchHi,这对寄存器用于设置硬件数据断点 (Hardware Data Breakpoint)。该断点一旦设定,当 CPU 存取这个地址时,系统就会发生一个异常。这个功能广泛应用于调试定位内存写坏的错误。
  • Register 28/29,TagLo 和 TagHi,用于高速缓存 (Cache) 管理。

#查看cp寄存器(openocd)

mon mips32 cp0

CP0 常用寄存器

下面,我们来详解 CP0 中常用的几个寄存器,它们是:BadVAddr,Count/Compare,Status/Cause,EPC,WatchLo/WatchHi。

MIPS 的异常与中断 (Section 2)

![[mips 大核手册#^7a2f66]]
其中,Exception 0-5, 8-11, 13, 23 这几个异常类型较为常见。

  • Exception 0:Interrupt,外部中断。

    它是唯一一个异步发生的异常。之所以说中断是异步发生的,是因为相对于其他异常来说,从时序上看,中断的发生是不可预料的,无法确定中断的发生是在流水线的哪一个阶段。MIPS 的五级流水线设计如下:
    IF, RD, ALU, MEM, WB。

    MIPS 处理器的中断控制部分有这样的设计:

    在中断发生时,如果该指令已经完成了 MEM 阶段的操作,则保证该指令执行完毕。反之,则丢弃流水线对这条指令的工作。除 NMI 外,所有的内部或外部硬件中断 (Hardware Interrupt) 均共用这一个异常向量(Exception Vector)。前面提到的 CP0 中的 Counter/Compare 这一对计数寄存器,当 Counter 计数值和 Compare 门限值相等时,即触发一个硬件中断。

  • Exception 1:TLB Modified,内存修改异常。

    如果一块内存在 TLB 映射时,其属性设定为 Read Only,那么,在试图修改这块内存内容时,处理器就会进入这个异常。显然,这个异常是在 Memory 阶段发生的。但是,按 “精确异常” 的原则,在异常发生时,ALU 阶段的操作均无效,也就是说,向内存地址中的写入操作,实际上是不会被真正执行的。这一判断原则,也适用于后面的内存读写相关的异常,包括 TLB Miss/Address Error/Watch 等。

  • Exception 2/3:TLB Miss Load/Write,

    如果试图访问没有在 MMU 的 TLB 中映射的内存地址,会触发这个异常。在支持虚拟内存的操作系统中,这会触发内存的页面倒换,系统的 Exception Handler 会将所需要的内存页从虚拟内存中调入物理内存,并更新相应的 TLB 表项。

  • Exception 4/5:Address Error Load/Write

    如果试图访问一个非对齐的地址,例如 lw/sw 指令的地址非 4 字节对齐,或 lh/sh 的地址非 2 字节对齐,就会触发这个异常。一般地,操作系统在 Exception Handler 中对这个异常的处理,是分开两次读取 / 写入这个地址。虽然一般的操作系统内核都处理了这个异常,最后能够完成期待的操作,但是由于会引起用户态到内核态的切换,以及异常的退出,当这样非对齐操作较多时会严重影响程序的运行效率。因此,编译器在定义局部和全局变量时,都会自动考虑到对齐的情况,而程序员在设计数据结构时,则需要对对齐做特别的斟酌。

  • Exception 6/7:Instruction/Data Bus Error

    一般地原因是 Cache 尚未初始化的时候访问了 Cached 的内存空间所致。因此,要注意在系统上电后,Cache 初始化之前,只访问 Uncached 的地址空间,也就是 0xA0000000-0xBFFFFFFF 这一段。默认地,上电初始化的入口点 0xBFC00000 就位于这一段。(某些 MIPS 实现中可以通过外部硬线连接修改入口点地址,但为了不引发无法预料的问题,不要将入口点地址修改为 Uncached 段以外的地址)

  • Exception 8:Syscall,系统调用的正规入口,也就是在用户模式下进入内核态的正规方式。

    我们可以类比 x86 下 Linux 的系统调用 0x80 来理解它。它是由一条专用指令 syscall 触发的。

  • Exception 9:Break Point,绝对断点指令。

    和 syscall 指令类似,它也是由专用指令 break 触发的。它指示了系统的一些异常情况,编程人员可以在某些不应当出现的异常分支里面加入这个指令,便于及早发现问题和调试。我们可以用高级语言中的 assert 机制来类比理解它。最常见的 Break 异常的子类型为 0x07,它是编译器在编译除法运算时自动加入的。如果除数为 0 则执行一条 break 0x07 指令。这样,当出现被 0 除的情况时,系统就会抛出一个异常,并执行 Coredump,以便于程序员定位除 0 错误的根因。

  • Exception 10:RI,执行了没有定义的指令,系统就会发生这个异常。

  • Exception 11,Co-Processor Unaviliable,试图访问的协处理器不存在。比如,在没有实现 CP2 的处理器上执行对 CP2 的操作,就会触发这个异常。

  • Exception 12,Overflow,算术溢出。会引起这个异常的指令,仅限于加减法中的带符号运算,如 add/addi 这样的指令。因此,一般地,编译器总是将加减法运算编译为 addiu 这样的无符号指令。由于 MIPS 处理异常需要一定的开销,这样可以避免浪费。

  • Exception 13,Trap,条件断点指令。

    它由 trap 系列指令引发。与 Break 指令不同的是,只有满足断点指令中的条件,才会触发这个异常。我们可以类比 x86 下的 int 3 断点异常来理解它。

  • Exception 14,VCEI,(不明白!谁知道是干嘛使的?)

  • Exceotion 15,Float Point Exception,浮点协处理器 1 的异常。它由 CP1 自行定义,与 CP1 的具体实现相关。其实就是专门为 CP1 保留的异常入口。

  • Exception 16,协处理器 2 的异常,和前一个异常一样,是和 CP2 的具体实现相关的。

  • Exception 23,Watch 异常。前面讲到 Watch 寄存器可以监控一段内存,当访问 / 修改这段内存时,就会触发这个异常。在异常处理例程中,通过异常栈可以反推出是什么地方对这段内存进行了读 / 写操作。这个异常是用来定位内存意外写坏问题的一柄利器。

MIPS 的异常与中断 (Section 3)

阅读全文 »

zMIPS体系结构采用的是 精确异常 处理模式
这是什么意思呢?下面来看从“See MIPS Run”一书中的摘录:

“In a precise-exception CPU, on any exception we get pointed at one instruction(the exception victim). All instructions preceding the exception victim in execution sequence are complete; any work done on the victim and on any subsequent instructions (BNN NOTE: pipeline effects) has no side effects that the software need worry about. The software that handles exceptions can ignore all the timing effects of the CPU’s implementations”

上面的意思其实很简单:在发生这个异常之前的一切计算行为会完整的结束并体现效果。 在发生这个异常之后的一切计算行为(包含当前这条指令)将不会产生任何效果。
另外一种解释是:

A precise exception is one in which the EPC (CP0, Register 14, Select 0) can be used to identify the instruction that caused the exception. For imprecise exceptions, the instruction that caused the exception cannot be identified. Most exceptions are precise. Bus error exceptions may be imprecise.

异常处理的一般过程

With the exception of Reset, Soft Reset, NMI, and Debug exceptions, which have their own special processing as described below, exceptions have the same basic processing flow:
• If the EXL bit in the Status register is cleared, theEPC register is loaded with the PC at which execution will be restarted and the BD bit is set appropriately in theCause register. If the instruction is not in the delay slot of a branch, the BD bit inCausewill be cleared and the value loaded into theEPCregister is the current PC. If the instruction is in the delay slot of a branch, the BD bit inCauseis set andEPCis loaded with PC-4.If the EXL bit in theStatus register is set, theEPCregister is not loaded and the BD bit is not changed in theCauseregister.
• The CE and ExcCode fields of the Cause registers are loaded with the values appropriate to the exception. The CE field is loaded, but not defined, for any exception type other than a coprocessor unusable exception.
• The EXL bit is set in the Status register.
• The processor is started at the exception vector.
The value loaded into EPC represents the restart address for the exception and need not be modified by exception handler software in the normal case. Software need not look at the BD bit in the Cause register unless is wishes to identify the address of the instruction that actually caused the exception. Note that individual exception types may load additional information into other registers. This is noted in the description of each exception type below.

EPC中存放的是异常发生时执行的指令地址,或者分支延时发生异常,则存放的是分支的指令地址,不管怎么样,异常处理函数返回都从EPC开始恢复执行,如果在分支延时指令发生异常,则需要在cause寄存器中存放相应标志,这样就可以准确的知道发生异常的指令地址了。

Operation:

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
ifStatus EXL= 0 then
if InstructionInBranchDelaySlot then
EPC <- PC - 4
Cause BD<- 1
else
EPC <- PC
Cause BD<- 0
endif
if ExceptionType = TLBRefill then
vectorOffset <- 0x000
elseif (ExceptionType = Interrupt) and
(Cause IV= 1) then
vectorOffset <- 0x200
else
vectorOffset <- 0x180
endif
else
vectorOffset <- 0x180
endif
Cause CE<- FaultingCoprocessorNumber
Cause ExcCode<- ExceptionType
Status EXL<- 1
if Status BEV= 1 then
PC <- 0xBFC0_0200 + vectorOffset
else
PC <- 0x8000_0000 + vectorOffset
endif

As with any procedure, the exception handler must save any registers it may modify, and then restore them before returning control to the interrupted program. Saving registers in memory poses a problem in MIPS:
addressing the memory requires a register (the base register) in which the address is formed. This means that a register must be modified before any register can be saved! The MIPS register usage convention (see Laboratory 4) reserves registers $26 and $27( $k0and$k1 ) for the use of the interrupt handler. This means that the interrupt handler can use these registers without having to save them first. A user program that uses these registers may find them unexpectedly changed.The CPU operates in one of the two possible modes,userandkernel.User programs run in user mode. The CPU enters the kernel mode when an exception happens. Coprocessor 0 can only be used in kernel mode.

阅读全文 »

来源

llvm项目 https://github.com/llvm/llvm-project , android aosp进行了定制

1
这是一个概念

概念

scudo 大概分为以下几个部分:

  1. 前端FrontEnd:使用Primary或Secondary后端来分配内存,还包括在初始化时根据配置(环境变量或者hook callback)来设置一些运行时参数:比如gc的时间、是否检查一些错误等。
  2. Primary后端: 对应aosp上用的是SizeClassAllocator64,负责小内存分配,最大支持0x10010大小的分配(这个大小还要包括chunk header的大小,且是对齐后的),Primary后端在初始化的时候就会为每个size class mmap 256M的VM(没有commit的)供前端使用。
  3. Secondary后端: 对于无法从Primary分配的大内存,这里会直接用mmap去分配。
  4. TSDRegistrySharedT:从名称能看出来这种tsd是多线程共享的。从下面的AndroidConfig能看到aosp上最多两个TSD(min(2, cpus)),即多个线程共享一个TSD中缓存的内存block。
  5. AndroidSizeClassMap:简单认为size class table的配置,即多少种slab,每种slab的元素的大小。slab是针对小内存提出的概念

配置

Android 添加了scudo的相关配置, 下面很多地方用到了这里面的配置

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
struct AndroidSizeClassConfig {
#if SCUDO_WORDSIZE == 64U
...
static constexpr u32 Classes[] = {
0x00020, 0x00030, 0x00040, 0x00050, 0x00060, 0x00070, 0x00090, 0x000b0,
0x000c0, 0x000e0, 0x00120, 0x00160, 0x001c0, 0x00250, 0x00320, 0x00450,
0x00670, 0x00830, 0x00a10, 0x00c30, 0x01010, 0x01210, 0x01bd0, 0x02210,
0x02d90, 0x03790, 0x04010, 0x04810, 0x05a10, 0x07310, 0x08210, 0x10010,
};
struct AndroidConfig {
using SizeClassMap = AndroidSizeClassMap;
#if SCUDO_CAN_USE_PRIMARY64
// 256MB regions 2^28 = 256M 一次map 256M * NumClasses 空间
typedef SizeClassAllocator64<SizeClassMap, 28U, 1000, 1000,
/*MaySupportMemoryTagging=*/true>
Primary;
#else
// 256KB regions 2^18 = 256k 一次map 256k * NumClasses 32位上
typedef SizeClassAllocator32<SizeClassMap, 18U, 1000, 1000> Primary;
#endif
// Cache blocks up to 2MB
typedef MapAllocator<MapAllocatorCache<256U, 32U, 2UL << 20, 0, 1000>>
Secondary;
template <class A>
using TSDRegistryT = TSDRegistrySharedT<A, 8U, 2U>; // Shared, max 8 TSDs.
setNumberOfTSDs((NumberOfCPUs == 0) ? DefaultTSDCount
: Min(NumberOfCPUs, DefaultTSDCount))
};

ChunkHeader

阅读全文 »

参考 hexdump-format_Focus-CSDN博客_hexdump格式化输出

hexdump -e '4/1 " 0x%02x,"' fdl_sec.bin -n 32

-e 指定格式字符串,格式字符串由单引号包含,格式字符串形如:’a/b “format1” “format2” ‘ 。每个格式字符串由三部分组成,每个由空格分割,如a/b表示,b表示对每b个输入字节应用format1格式,a表示对每个a输入字节应用format2,一般a>b,且b只能为1,2,4,另外a可以省略,省略a=1。format1和format2中可以使用类似printf的格斯字符串。

1
2
3
hexdump -e '16/1 " 0x%02x," "\n" ' keys/aes_priv_key
0x34, 0xf1, 0x4b, 0xd9, 0x1b, 0x32, 0x0f, 0x05, 0xb1, 0x5c, 0x6e, 0x72, 0x9d, 0xec, 0x42, 0x74,
0xd1, 0xae, 0x29, 0x36, 0x53, 0xe1, 0x89, 0x86, 0xd8, 0x88, 0xfe, 0xcc, 0x39, 0xbb, 0x30, 0x81,

-v 打印重复项

如果不带-v, 遇到重复的 会以 * 显示, 带 -v会显示所有项

-n 打印的字节数

-s 忽略文件开头多少个字节,再开始打印数据

因此最终可以使用下面命令完整打印单字节打印二进制文件的数据, 并可以无缝将数据导入到数组中

1
hexdump -e '16/1 " 0x%02x," "\n" ' -v <file> -n <字节数目> -s <字节数目>
阅读全文 »

jemalloc中提供的malloc函数叫做je_malloc, 释放的函数是je_free.

jemalloc基础知识

size_class

每个 size_class 代表 jemalloc 分配的内存大小,共有 NSIZES(232)?个小类(如果用户申请的大小位于两个小类之间,会取较大的,比如申请14字节,位于8和16字节之间,按16字节分配),分为2大类:

  • small_class小内存) : 对于64位机器来说,通常区间是 [8, 14kb],常见的有 8, 16, 32, 48, 64, …, 2kb, 4kb, 8kb,注意为了减少内存碎片并不都是2的次幂,比如如果没有48字节,那当申请33字节时,分配64字节显然会造成约50%的外部碎片
  • large_class大内存): 对于64位机器来说,通常区间是 [16kb, 7EiB],从 4 * page_size 开始,常见的比如 16kb, 32kb, …, 1mb, 2mb, 4mb等
  • size_index : size 位于 size_class 中的索引号,区间为 [0,231]?,比如8字节则为0,14字节(按16计算)为1,4kb字节为28,当 size 是 small_class 时,size_index 也称作 binind

base

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
struct base_s {
/* Associated arena's index within the arenas array. */
unsigned ind;
/* User-configurable extent hook functions. Points to an extent_hooks_t. */ extent分配回收等相关的函数指针
atomic_p_t extent_hooks;

/* Protects base_alloc() and base_stats_get() operations. */
malloc_mutex_t mtx;

/* Using THP when true (metadata_thp auto mode). */
bool auto_thp_switched;
/*
* Most recent size class in the series of increasingly large base
* extents. Logarithmic spacing between subsequent allocations ensures
* that the total number of distinct mappings remains small.
*/
pszind_t pind_last;

/* Serial number generation state. */ 下一个extent的sn号
size_t extent_sn_next;

/* Chain of all blocks associated with base. */ blocks链表
base_block_t *blocks;

/* Heap of extents that track unused trailing space within blocks. */ extent堆的root节点[NSIZES]
extent_heap_t avail[NSIZES];

/* Stats, only maintained if config_stats. */ 统计信息相关
size_t allocated;
size_t resident;
size_t mapped;
};

/* Embedded at the beginning of every block of base-managed virtual memory. */
struct base_block_s base_block_t {
/* Total size of block's virtual memory mapping. */
size_t size;

/* Next block in list of base's blocks. */
base_block_t *next;

/* Tracks unused trailing space. */ 保存extent的元数据信息
extent_t extent;
};

用于分配 jemalloc 元数据内存的结构

  • base.blocks.extent : 存放每个 size_classextent 元数据
  • base.blocks是一个链表, 通过其next可以找到当前ind维护的所有的extent元数据信息

bin

阅读全文 »

scudo_allocate.drawio

下面的参考数据仅是指64位程序的, 32位的配置和64位不同.

  • M_TSDS_COUNT_MAX

    设置线程局部存储(TSL)相关的TSD的数目, M_TSDS_COUNT_MAX设置为8后, 如小于等于8个线程, 每个线程会绑定一个TSD.

    在每个线程中 使用malloc free时, 会根据线程状态寄存器查找到其绑定的TSD缓存池.

    ​ scudo没有外部配置的情况下, 只有两个TSD缓存池, 在配置M_TSDS_COUNT_MAX后, 会产生最多M_TSDS_COUNT_MAX个缓存池, 该值会影响 small(primary) 分配器. 在TSD缓存池少的情况下, 会出现多个线程共用一个TSD缓存池的情况, 分配释放内存时需要等锁.

    Secondary 分配器没有绑定TSD(不是线程私有), 是所有线程共用同一块缓存池, 分配释放需要加锁等锁.

    jemalloc5 中 在sz index(36-45范围内的), 当前的配置字节在(14336- 65536)) 使用的缓存是绑定tcache的(线程私有).

  • M_CACHE_COUNT_MAX

    option->MaxCacheEntriesCount

    large(Secondary) 分配器在分配释放内存时, 所有线程共用的缓存池的容量, 每次free 时, 会先将该内存单元放到缓存池中, 下次malloc时, 会优先根据分配的size 在缓存池中查找是否有匹配的缓存内存单元, 如果有则直接把该内存单元的地址返回给调用方.

    large(Secondary)的缓存池在满了以后, 下一次free时, 会重置清空缓存池

    该值对应上图的 EntriesArraySize, 默认值为32, 最大只能设置到256, 如果超过256, 会设置失败, 用默认值.

  • M_CACHE_SIZE_MAX

    option->MaxCacheEntrySize

    在free时, large(Secondary) 分配器并不是将所有大于small分配器的内存单元全部放到缓存池中, 而是在 0x40010-M_CACHE_SIZE_MAX范围内的会在free时放到缓存池中(small的范围0-0x40010), malloc时会优先从缓存池中查找. 而大于M_CACHE_SIZE_MAX的malloc则直接走mmap, free走unmap.

    该值对应上图的 MaxEntrySize, 默认值是2M

阅读全文 »

以SIGSEGV为例详解信号处理(与栈回溯)

信号是内核提供的向用户态进程发送信息的机制, 常见的有使用SIGUSR1唤醒用户进程执行子程序或发生段错误时使用SIGSEGV保存用户错误现场. 本文以SIGSEGV为例, 详细分析信号使用方法, 内核信号的发送与接收机制.

信号处理例程

以下是一个SiGEGV处理例程, 主程序注册一个信号量并创建一个线程, 线程中故意访问空指针, 引发段错误. 在信号回调中会回溯堆栈, 保存出错的地址.
回溯堆栈的原理在分析完整个信号处理流程后再分析, 首先我们先来分析如何使用信号.

sigaction()用于向内核注册一个信号(参数1), 使用参数2(如果非空)作为注册信号的回调, 内核会将之前的信号回调返回在参数3中(如果非空). 如果父进程或程序之前阻塞了该信号则需先调用sigprocmask()取消阻塞.
在回调处理结束时需手动退出进程(exit()), 否则内核会不断触发该信号(重新执行异常指令再次引起崩溃), glibc对SIGSEGV有默认的回调, 所以默认情况下也会正常退出.

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
#include <string.h>
#include <signal.h>
#include <stdio.h>
#include <unistd.h>
#include <pthread.h>
#define POPCNT(data)                            do {        \
        data = (data & 0x55555555) + ((data >> 1) & 0x55555555);    \
        data = (data & 0x33333333) + ((data >> 2) & 0x33333333);    \
        data = (data & 0x0F0F0F0F) + ((data >> 4) & 0x0F0F0F0F);    \
        data = (data & 0x00FF00FF) + ((data >> 8) & 0x00FF00FF);    \
        data = (data & 0x0000FFFF) + ((data >> 16) & 0x0000FFFF);    \
    } while (0);
/**
 * we only calculate sp decrease which is static confirm in compile time
 * that is sub immediate & push instruction(and return when we find push)
 *
**/
void backtrace_stack(unsigned int **pppc, unsigned int **ppsp)
{
    unsigned int *ppc_last = *pppc;
    unsigned int *psp = *ppsp;
    unsigned int decrease = 0;
    int i;
    enum
    {
        INS_SUB_IMM = 0,
        INS_STM1,
        INS_STR_LR,
        INS_STR_FP,
        INS_BUTT
    };
    //see ARM reference manual for more detail
    struct ins_map
    {
        unsigned int mask;
        unsigned int ins;
    };
    struct ins_map map[INS_BUTT] =
    {
        {0xFFEFF000, 0xE24DD000},
        {0xFFFF4000, 0xE92D4000},
        {0xFFFFFFFF, 0xE52DE004},
        {0xFFFFFFFF, 0xE52DB004},
    };
again:
    ppc_last--;
    for (i = 0; i < INS_BUTT; i++)
    {
        if (map[i].ins == (*ppc_last &map[i].mask))
        {
            break;
        }
    }
    switch (i)
    {
    case INS_SUB_IMM:
        //sub sp, sp, imm
        decrease = (*ppc_last & 0xFF) << ((32 - 2 * (*ppc_last & 0xF00)) % 32);
        psp += decrease / sizeof(unsigned int);
        break;
    case INS_STM1:
        //push lr, ...
        decrease = *ppc_last & 0xFFFF;
        POPCNT(decrease);
        psp += decrease;
        *pppc = *(psp - 1);
        *ppsp = psp;
        return;
    case INS_STR_LR:
        //push lr
        psp += 1;
        *pppc = *(psp - 1);
        *ppsp = psp;
        return;
    case INS_STR_FP:
        //push fp
        psp += 1;
        *ppsp = psp;
        return;
    default:
        break;
    }
    goto again;
}
/**
 * process stack when catch a sigsegv:
 * ------------   stack top
 * | ......
 * | fault addr   sp position when memory fault happen
 * | sigframe     kernel use to resotre context DO NOT MODIFY(same to data)
 * | siginfo      glibc push this struct into stack(same to siginfo)
 * | current sp   sp position when enter signal handle
 *
**/
void sighandle(int sig, siginfo_t *siginfo, void *data)
{
    //data point to sigframe which is not seen to user
    //search struct ucontext in kernel for more detail
    unsigned int *psp = ((unsigned int *)data) + 21;
    unsigned int *plr = ((unsigned int *)data) + 22;
    unsigned int *ppc = ((unsigned int *)data) + 23;
    unsigned int pc_val[5] = {0};
    unsigned int sp_val[5] = {0};
    char **ppstr;
    int i;

    printf("get signal %u addr %x\n", siginfo->si_signo, siginfo->si_addr);
    pc_val[0] = *ppc;
    sp_val[0] = *psp;
    for (i = 1; i < 4; i++)
    {
        pc_val[i] = pc_val[i - 1];
        sp_val[i] = sp_val[i - 1];
        backtrace_stack((unsigned int **)(&pc_val[i]), (unsigned int **)(&sp_val[i]));
        /**
         * for subroutine use push {fp} instruction, we can't get it's caller pc
         * so we use last lr as pc and hope program won't push {fp} twice
         *
        **/
        if (pc_val[i] == pc_val[i - 1])
        {
            pc_val[i] = *plr;
        }
        pc_val[i] -= 4;
    }
    ppstr = backtrace_symbols((void **)pc_val, 5);
    for (i = 0; i < 5; i++)
    {
        printf("%u: pc[0x%08x] sp[0x%08x] %s\n", i, pc_val[i], sp_val[i], ppstr[i]);
    }
    exit(1);
}
void fault_func3()
{
    int *p = NULL;
    *p = 1;
}
void fault_func2()
{
    int a = 0x5678;
    fault_func3();
    return;
}
void fault_func1(void *pvoid)
{
    int a = 0x1234;
    fault_func2();
    return;
}
int main(int argc, char *argv[])
{
    struct sigaction sigact;
    int *p = NULL;
    memset(&sigact, 0, sizeof(struct sigaction));
    sigact.sa_sigaction = sighandle;
    sigact.sa_flags = SA_SIGINFO | SA_RESTART;
    sigaction(SIGSEGV, &sigact, NULL);
    getc(stdin);
    pthread_t thread;
    pthread_create(&thread, NULL, fault_func1, NULL);
    while (1)
    {
        ;
    }
    return 0;
}

内核信号量数据结构与系统调用

虽然用户调用的sig*接口都是glibc的接口, 但实际上glibc还是通过系统调用实现的.
与信号量相关的数据结构有:
task_struct(负责保存信号处理句柄, 阻塞与挂起的信号队列)
sighand_struct(每个信号处理 handler句柄, 保护信号的自旋锁)
signal_struct(信号量结构, 大部分参数都在该结构中)
sigpending(挂起队列, 用于索引挂起的信号)
作为一种信息传递机制, 信号量代码本身并不复杂, 即使是信号发送接口__send_signal()(分析见下).

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
50
51
52
53
54
55
56
57
58
59
60
61
62
63
struct task_struct {
......

struct signal_struct *signal;
//信号处理句柄, 包括每个信号的action, 锁与等待队列
struct sighand_struct *sighand;
//该task阻塞的信号
sigset_t blocked, real_blocked;
sigset_t saved_sigmask;
//该task挂起信号的结构体
struct sigpending pending;
......
};

struct sighand_struct {
atomic_t count;
//保存信号处理句柄的数组
struct k_sigaction action[_NSIG];
//自旋锁, 不仅保护该结构同时还保护task_struct.signal
spinlock_t siglock;
wait_queue_head_t signalfd_wqh;
};

/**
\* signal_struct自身没有锁
\* 因为一个共享的signal_struct往往对饮一个共享的sighand_struct
\* 即使用sighand_struct的锁是signal_struct的超集
*
**/
struct signal_struct {
......
//进程的信号挂起队列, 与task_struct.pending区别是所有线程共享
struct sigpending shared_pending;
......
};

//描述挂起信号的结构体
//成员list为进程所有挂起信号的双线链表的头
//成员signal为进程挂起信号量的位图, 挂起的信号对应的位置位
struct sigpending {
//sigqueue链表头
struct list_head list;
//当前挂起的信号量位图
sigset_t signal;
};

//描述一个挂起信号的结构体
struct sigqueue {
//sigqueue链表节点
struct list_head list;
int flags;
//该挂起信号的信息
siginfo_t info;
struct user_struct *user;
};

//描述信号相关信息的结构体
typedef struct siginfo {
int si_signo;
int si_errno;
int si_code;
......
} __ARCH_SI_ATTRIBUTES siginfo_t;
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
/**
 * 定义见kernel/signal.c
 * 获取或修改拦截的信号
 * @how: 为SIG_BLOCK / SIG_UNBLOCK / SIG_SETMASK的一种
 * @nset: 如果非空为增加或移除的信号
 * @oset: 如果非空为之前的信号
 * note: sigprocmask系统调用任务很简单, 用新值修改current->blocked并将旧值传回用户态
 *       调用set_current_blocked中会先剔除SIGKILL与SIGSTOP, 用户传递这两个值是无效的
 *       之后还会判断task是否已经pending及是否有线程, 如果有还需对每个线程单独处理
 *
**/
SYSCALL_DEFINE3(sigprocmask, int, how, \
    old_sigset_t __user *, nset, \
    old_sigset_t __user *, oset);
/**
 * 定义见kernel/signal.c
 * 获取或修改拦截信号的action
 * @sig: 为拦截的信号
 * @act: 如果非空为信号sig的action
 * @oact: 如果非空为返回之前信号sig的action
 * note: 如果传入未定义信号或SIGKILL与SIGSTOP会直接返回EINVAL
 *       如果act非空则将其赋值给进程task_struct.sighand->action[i]中
 *       然后检测所拦截的信号是否挂起, 如果有挂起则将其从队列中删除
 *
**/
SYSCALL_DEFINE3(sigaction, int, sig, \
    const struct old_sigaction __user *, act, \
    struct old_sigaction __user *, oact);
/**
 * 定义见kernel/signal.c
 * 以下两接口为发送信号的接口, 实际调用send_signal
 * send_signal()调用__send_signal
 *
**/
int do_send_sig_info(int sig, struct siginfo *info, \
    struct task_struct *p, bool group);
int __group_send_sig_info(int sig, \
    struct siginfo *info, struct task_struct *p);
阅读全文 »

1. arm64 寄存器和指令

1.1. 寄存器

arm64 有34个寄存器, 包含31个通用寄存器, SP PC CPSR

寄存器中可以存地址, 也可以存值.

寄存器 位数 描述
x0-x30 64 通用寄存器, 可以作 位使用: w0-w30 (访问寄存器的低位)
x0-x7 64 用于子程序调用时参数传递(形参), x0 还用于返回值传递
FP(x29) 64 当前栈帧的栈底 (android上 向上增长), 栈顶地址 < 栈底
LR(x30) 64 程序链接寄存器, 保存子程序结束后需要执行的下一条指令. 即谁调用了当前函数.
SP 64 保存栈指针, 使用SP/WSP来进行对栈寄存器的访问
PC 64 程序计数器, 俗称PC指针, 总是指向即将执行的下一条指令, 在arm64中, 软件不能修改pc寄存器
CPSR 64 状态寄存器, NZCV是状态寄存器的条件标志位,分别代表运算过程中产生的状态

我们可以根据FP和SP寄存器回溯函数调用过程,通过这两个值,我们可以知道函数的栈起始地址(也就是FP寄存器的值), 以及栈顶(也就是SP寄存器的值)。得到了m函数的栈帧,就很容易从里面提取LR寄存器的值了(FP向下偏移8个字节即为LR),也就知道了谁调用了当前函数。以此类推,可以得到一个完整的函数调用链(一般回溯到 main函数或者线程入口函数就没必要继续了)。实际上,回溯过程中我们并不需要知道栈顶SP,只要FP就够了

https://juejin.im/post/6844903923719864333

1.1.1. arm64 约定

  • x0 ~ x7 分别会存放方法的前 8 个参数;如果参数个数超过了8个,多余的参数会存在栈上,新方法会通过栈来读取。
  • 方法的返回值一般都在 x0 上;如果方法返回值是一个较大的数据结构时,结果会存在 x8 执行的地址上。

1.2. 虚存地址与elf 段地虚址

阅读全文 »

实现思路

沿着内存dump的步骤

small slab

先忽略tcache的话, 需要关注的结构 je_arenas, arena, bins , bin->slabcur bin->slabs_nofull, bin->slabs_full, je_bin_infos extent_s

1
2
3
4
5
6
7
8
p je_arenas
p (*(struct arena_s*)je_arenas[0])
p (*(struct arena_s*)je_arenas[0]).bins
p (*(struct arena_s*)je_arenas[0]).bins[0] 0-NSIZES
p je_bin_infos
p (*(struct arena_s*)je_arenas[0]).bins[0].slabcur
p *(struct extent_s*) [p (*(struct arena_s*)je_arenas[0]).bins[0].slabcur] #取slabcur 的slab
p (*(struct arena_s*)je_arenas[0]).extents_dirty

extent 元信息中关于ind的信息?

extent_szind_get

(extent->e_bits & EXTENT_BITS_SZIND_MASK) >> EXTENT_BITS_SZIND_SHIFT

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
EXTENT_BITS_ARENA_WIDTH = 12 ;
EXTENT_BITS_ARENA_SHIFT =0;
EXTENT_BITS_ARENA_MASK =((((1 << (12)) - 1)) << (0)); //4095 111111111111 fff
EXTENT_BITS_SLAB_WIDTH =1;
EXTENT_BITS_SLAB_SHIFT =(12 + 0);
EXTENT_BITS_SLAB_MASK =((((1 << (1)) - 1)) << ((12 + 0))); //4096 1000000000000 1000
EXTENT_BITS_COMMITTED_WIDTH = 1;
EXTENT_BITS_COMMITTED_SHIFT = (1 + (12 + 0));
EXTENT_BITS_COMMITTED_MASK =((((1 << (1)) - 1)) << ((1 + (12 + 0)))); //8192 10000000000000 2000
EXTENT_BITS_DUMPABLE_WIDTH = 1;
EXTENT_BITS_DUMPABLE_SHIFT = (1 + (1 + (12 + 0))) ;
EXTENT_BITS_DUMPABLE_MASK =((((1 << (1)) - 1)) << ((1 + (1 + (12 + 0))))); //16384 100000000000000 4000
EXTENT_BITS_ZEROED_WIDTH =1;
EXTENT_BITS_ZEROED_SHIFT =(1 + (1 + (1 + (12 + 0)))); //15 1111 f
EXTENT_BITS_ZEROED_MASK = ((((1 << (1)) - 1)) << ((1 + (1 + (1 + (12 + 0)))))); //32768 1000000000000000 8000
EXTENT_BITS_STATE_WIDTH = 2;
EXTENT_BITS_STATE_SHIFT = (1 + (1 + (1 + (1 + (12 + 0))))); //16
EXTENT_BITS_STATE_MASK =((((1 << (2)) - 1)) << ((1 + (1 + (1 + (1 + (12 + 0))))))); //196608 110000000000000000 30000
EXTENT_BITS_SZIND_WIDTH =8;
EXTENT_BITS_SZIND_SHIFT =(2 + (1 + (1 + (1 + (1 + (12 + 0)))))); // 18 10010 12
EXTENT_BITS_SZIND_MASK =((((1 << (8)) - 1)) << ((2 + (1 + (1 + (1 + (1 + (12 + 0)))))))); // 66846720 11111111000000000000000000 3FC0000
EXTENT_BITS_NFREE_WIDTH =((12 - 3) + 1); //10 1010 a
EXTENT_BITS_NFREE_SHIFT =(8 + (2 + (1 + (1 + (1 + (1 + (12 + 0))))))); //26 11010 1a
EXTENT_BITS_NFREE_MASK =((((1 << (((12 - 3) + 1))) - 1)) << ((8 + (2 + (1 + (1 + (1 + (1 + (12 + 0))))))))); // 68652367872 111111111100000000000000000000000000 FFC000000
EXTENT_BITS_SN_SHIFT =(((12 - 3) + 1) + (8 + (2 + (1 + (1 + (1 + (1 + (12 + 0)))))))); // 36 100100 24
EXTENT_BITS_SN_MASK =(18446744073709551615 << (((12 - 3) + 1) + (8 + (2 + (1 + (1 + (1 + (1 + (12 + 0))))))))); // FFFFFFFFFFFFFFFF000000000
1
2
e_bits = 29863505920
11011110100000000001111000000000000

通过rtree 信息直接检索所有的extent

1
2
3
p je_extents_rtree.root #遍历R树 是一个1<<18 容量的数组, root是头指针
p je_extents_rtree.root[x].child # 每一个都是1个 1<<18容量的数组, child是头指针, 需要遍历非0x0的有效数
#最后找出有效的叶子节点
阅读全文 »