问题提出

20230112-151134.jpeg

前几天在帮忙事务同学排查事务问题时,遇到了一个奇怪的问题。实践的事务代码如上,默认定义了一个部分变量 variable ,经过另一个开关操控这个参数是否为 NO。但观察线上数据,在开关为 NO 时依旧有参数为 YES 的上报,数量尽管之前版别一向存在但量级较小,视为差错。但最新版别上线后发现这种状况量级突然变大,可是这个函数的代码没有任何改动。

排除了其他的或许性之后,问题就来到了置疑 variable 是否值不确认,也便是是否或许是 NO 或许 YES。

于是同学询问:Objective-c BOOL 默认值在所有体系都是NO吧?

这一下子就把我问住了,我有形象应该这个在部分变量的时分是未定义的,但非部分变量的状况下我也不是很清楚。我就问了问同组的同学,发现咱们关于部分的 BOOL 变量的默认值是什么也有争议,部分同学以为永远是 NO。既然有疑问,那咱们就需求进行研究,趁便小水一篇

咱们进一步笼统这个问题,在 ARC 环境下,下面这段代码的各个变量的值是什么?

- (void)sample {
    NSObject *obj;
    BOOL boolean;
    NSInteger inter;
    char *pointer;
    static BOOL staticBoolean;
}

关于阅览本文需求的前置常识,因为篇幅较大,为了咱们的阅览体会挪到了本文的终究,假如对 C 的内存布局 与 ARM64 汇编 不熟悉的同学能够先跳转观看。

经过汇编进行验证管用类型

剖析汇编

咱们能够简略写一个 Demo 验证一下 BOOL 的状况。

- (void)viewDidLoad {
    [super viewDidLoad];
    for (NSInteger i = 0; i < 1000; i++) {
        BOOL defined = [self defined];
        BOOL undefined = [self undefined];
        NSAssert(undefined == NO, @""); // 假如以为 BOOL 未经初始化一定是 NO 的话,那这个 Assert 就永远不会中 
    }
}
- (BOOL)defined {
    BOOL defined = YES;
    return defined;
}
- (BOOL)undefined{
    BOOL undefined; 
    return undefined; // Variable 'undefined' is uninitialized when used here
}

跑起来看看成果,Demo 因为射中 Assert 后抛出 NSException 而溃散了。因而能够先简略承认定论了。未初始化的暂时变量 BOOL 的值并非确认是 NO。

image

但为什么是未定义的?这就需求汇编来协助咱们了。

在 ARM64 的真机 Debug 下 disassemble 一下,假如用的模拟器则会呈现 x86 的汇编,感兴趣的同学能够自行测验,咱们这儿就以 ARM64 的汇编为例了:

- (BOOL)undefined {
    BOOL undefined;
->  return undefined;
}
(lldb) disassemble
demo`-[ViewController undefined]:
    0x102fade68 <+0>:  sub    sp, sp, #0x20 // sp = sp - 20,push stack,自身因为调用 @selector(undefined) 新开了一个函数栈,至于为什么是 0x20 是因为 sp 要以 16(0x10) 字节对齐:除了 存 x0(8 字节) x1(8 字节) ,BOOL (1 个字节,读到寄存器再补成了 4 个字节变成了 w8) ,还有 15 个字节便是对齐的成本
    0x102fade6c <+4>:  str    x0, [sp, #0x18] // 把 x0 存到 sp + 0x18 的地址,x0 实践是 self,存储的详细方位是 sp + 0x18 到 sp + 0x20 的这 8 个字节
    0x102fade70 <+8>:  str    x1, [sp, #0x10] // 把 x1 存到 sp + 0x18 的地址,x1 实践是 selector(undefined),存储的详细方位是 sp + 0x10 到 sp + 0x18 这个方位。
->  0x102fade74 <+12>: ldrb   w8, [sp, #0xf] // w8 直接从 sp + 0xf 的地址读取,可是 sp + 0xf 的方位是未定义的,w8 便是 undefined 这个变量
    0x102fade78 <+16>: and    w0, w8, #0x1 // 把 w8 与 0x1 取并(也便是成果要么 0x0 要么 0x1)塞给 w0(x0),回来给外部
    0x102fade7c <+20>: add    sp, sp, #0x20
    0x102fade80 <+24>: ret   

咱们来看下内存分布,加深下对 stack 内存布局的了解,一句话了解定论便是:分给 undefined 的内存 sp + 0xf 的值是未定义的,因而导致了 undefined 的值也是未定义的。

(lldb) reg read sp
      sp = 0x000000016d8e79a0

咱们首要打印 sp 的值,值为 0x000000016d8e79a0

(lldb) memory read 0x000000016d8e79a0 0x000000016d8e79c0
                                                        sp+0xf
                                                          ⬇️
0x16d8e79a0: 01 00 00 00 00 00 00 00 00 d0 51 02 01 00 00 11  ..........Q.....
              x1  sp+0x10 ~ sp+0x18   x0  sp+0x18 ~ sp+0x20                   
                        ⬇️                       ⬇️                            
0x16d8e79b0: 78 4e 74 b1 01 00 00 00 f0 61 60 17 01 00 00 00  xNt......a`.....

接着咱们检查内存中从 0x000000016d8e79a0(sp) 到 0x000000016d8e79c0(sp + 0x20) 的详细内容,经过 memory read 指令,也便是 -[ViewController undefined] 这个函数本次分配到的栈内存。

每个参数寄存器对应写入的内存地址与范围现已标示在上方了,例如 x0 对应 sp+0x18 ~ sp+0x20 ,x1 对应 sp+0x10 ~ sp+0x18

(lldb) po self
<ViewController: 0x1176061f0>
(lldb) reg read x0
      x0 = 0x00000001176061f0

x0 首要肯定是外部调用的 self,也便是本次的 ViewController ,咱们看到因为 ARM64 是小端序,因而展现在内存里的值与咱们正常阅览的次序是反过来的,详细来说便是每个 Byte 位内部不用倒置,可是全体阅览的时分需求反转,例如 x0 对应的内存区域:sp+0x18 ~ sp+0x20f0 61 60 17 01 00 00 00 -> 00 00 00 01 17 60 61 f0 -> 0x00000001176061f0

(lldb) reg read x1
      x1 = 0x00000001b1744e78
(lldb) memory read 0x00000001b1744e70
0x1b1744e70: 63 74 56 61 6c 75 65 00 75 6e 64 65 66 69 6e 65  ctValue.undefine
0x1b1744e80: 64 00 54 42 2c 52 2c 4e 2c 47 69 73 55 6e 64 65  d.TB,R,N,GisUnde

同理咱们能够看到 x1 是调用时的 @selector ,写入了内存中 sp+0x18 ~ sp+0x20 的方位,相同能够看到是能对应上的,咱们还能够看 x1 指向的内存区域,看看 @selector 是什么,能够看到是 undefine\0 。终究的 \0 是表达 Cstr 的完毕。

(lldb) memory read 0x000000016d8e79a0 0x000000016d8e79c0
                                                        sp+0xf
                                                          ⬇️
0x16d8e79a0: 01 00 00 00 00 00 00 00 00 d0 51 02 01 00 00 11  ..........Q.....
// 下面一行略

接着咱们再剖析 w8 的诞生,w8 便是对 undefined 这个变量的操作,w8 从 sp + 0xf 读取这一个字节,本次运转的时分读取到的值是 0x11 ,然后 ldrb 会高位补 0 ,因而 w8 的成果便是 4 个字节的 0x11 。终究再对 w8 取 与 0x1 ,获得了终究的 BOOL 值。

这儿尽管本次取到的值是 0x11 ,但这个是完全随机的,原因咱们在一开始也就讲过了,Stack 的内存内容是完全随机,而且咱们的程序也没有对其进行初始化。本来操作体系分给你是什么便是什么,不论是 heap 仍是 stack 都相同。关于 Stack 来说,这块内存或许之前有更深的调用使用过,在 pop 的时分并不会清零。

A common assumption made by novice programmers is that all variables are set to a known value, such as zero, when they are declared. While this is true for many languages, it is not true for all of them, and so the potential for error is there. Languages such as C use stack space for variables, and the collection of variables allocated for a subroutine is known as a stack frame. While the computer will set aside the appropriate amount of space for the stack frame, it usually does so simply by adjusting the value of the stack pointer, and does not set the memory itself to any new state (typically out of efficiency concerns). Therefore, whatever contents of that memory at the time will appear as initial values of the variables which occupy those addresses.

en.wikipedia.org/wiki/Uninit…

咱们再对比下有默认值的汇编,看看有什么差异:

- (BOOL)undefined {
    BOOL defined = YES;
->  return defined;
}
(lldb) dis
demo`-[ViewController undefined]:
    0x100b29e60 <+0>:  sub    sp, sp, #0x20
    0x100b29e64 <+4>:  str    x0, [sp, #0x18]
    0x100b29e68 <+8>:  str    x1, [sp, #0x10]
    0x100b29e6c <+12>: mov    w8, #0x1 // 先把 w8 设置成 YES
    0x100b29e70 <+16>: strb   w8, [sp, #0xf] // 把 w8 存到 sp + 0xf 的方位
->  0x100b29e74 <+20>: ldrb   w8, [sp, #0xf]
    0x100b29e78 <+24>: and    w0, w8, #0x1
    0x100b29e7c <+28>: add    sp, sp, #0x20
    0x100b29e80 <+32>: ret    

能够看到主要差异在于 w8 有初始化赋值,其他都没有差异。于此,关于管用类型的初始化验证完毕。

网络上寻觅相关材料

咱们看最权威的材料:”ISO/IEC 9899:TC3 (Current C standard)” 中 Section 6.7.8 关于 Initialization 的论说。咱们在 stack 上分配的变量是 automatic storage,而且是没有经过初始化的,这种值是未定义的。

image

一起后半句还说到,假如是 static storage 而且未初始化:

  1. 指针类型便是空指针;

  2. 算术类型 (int / float 等) 便是 0 ;

  3. 复合类型 (struct / class) 的每个成员变量曾经面的规则初始化;

  4. 关于 union ,只要第一个命名的成员曾经面的规则初始化,其他的都是未定义,能够看:Static storage union and named members initialization in C language。

定论得出

至此咱们就能够解答同学提出的疑问了,关于部分变量且是管用类型时,这个值是不确认的。这个定论也契合现象,之前版别也一向存在这种上报,可是最新版别或许因为恣意调用链路上的 stack 调用改动,导致 BOOL 值变成 YES 的概率变大。同理,假如是 NSInteger 或许 CGFloat 或许 其他类型也是相同的成果。

20230112-151134.jpeg

一起咱们发现这么写代码,Xcode 也有对应的警告,能够看下 Clang 相关的静态检查

  • core.uninitialized.ArraySubscript (C)

  • core.uninitialized.Assign (C)

  • core.uninitialized.Branch (C)

  • core.uninitialized.CapturedBlockVariable (C)

  • core.uninitialized.UndefReturn (C)

  • core.uninitialized.NewArraySize (C++)

clang.llvm.org/docs/analyz…

咱们能够考虑在项目的 CI 中敞开对应的检查,或许本地敞开 Warn As Error。

再进一步拓宽到一般指针与 NSObject (OC 目标)指针

一般指针

其实根据方才上面的剖析,咱们现已能够大致猜到一般指针的成果,因为分配的区域是 stack ,而 stack 又未经初始化,因而大概率一般的指针默认值也不是 nil 。

- (void)viewDidLoad {
    for (NSInteger i = 0; i < 100; i++) {
        NSAssert([self voidPointer] == nil, @"");
    }
}
- (char *)voidPointer {
    char *voidPointer;
->  return voidPointer;
}
(lldb) dis
demo`-[ViewController voidPointer]:
    0x104c1dcb0 <+0>:  sub    sp, sp, #0x20
    0x104c1dcb4 <+4>:  str    x0, [sp, #0x18]
    0x104c1dcb8 <+8>:  str    x1, [sp, #0x10]
->  0x104c1dcbc <+12>: ldr    x0, [sp, #0x8]
    0x104c1dcc0 <+16>: add    sp, sp, #0x20
    0x104c1dcc4 <+20>: ret    

与之前的剖析基本相同,仅仅因为 char * 自身是一个指针,仅仅偏移量有所改动,这次 voidPointer 取的是 sp + 0x8 到 sp + 0x10 而已。因而值也是不确认的。与咱们设想的共同。

image

NSObject 指针

ARC

到了这时分咱们或许会觉得,NSObject 指针有啥不相同的,不也是在 stack 上分配内存吗,那肯定值是不确认的。

了解到这儿我只能说确实是了解了我前文的内容,可是关于 OC 的了解还有欠缺(手动狗头)。

- (void)viewDidLoad {
    [super viewDidLoad];
    for (NSInteger i = 0; i < 1000; i++) {
        NSAssert([self nsobjectPointer] == nil, @"");
    }
}
- (NSObject *)nsobjectPointer {
    NSObject *object;
    return object;
}

咱们跑起来看下,竟然没有射中 Assert ,看起来 NSObject * 不初始化也能是 null ?那这是为什么呢?(其实标题现已剧透了

首要咱们对这个文件的编译参数是 ARC ,因而关于所有的指针类型默认特点都是 __strong 。

其实就隐藏在 ARC 帮咱们自动插入的代码中,咱们也相同来看看汇编。 为了简化代码,咱们把函数回来值取消(不然你会看到比如 objc_autoreleaseReturnValue 的调用,添加杂乱度)。

- (void)nsobjectPointer {
    NSObject *object;
->  
}
(lldb) dis
demo`-[ViewController nsobjectPointer]:
    0x102f05db8 <+0>:  sub    sp, sp, #0x30
    0x102f05dbc <+4>:  stp    x29, x30, [sp, #0x20]
    0x102f05dc0 <+8>:  add    x29, sp, #0x20
    0x102f05dc4 <+12>: stur   x0, [x29, #-0x8]
    0x102f05dc8 <+16>: str    x1, [sp, #0x10]
    0x102f05dcc <+20>: add    x0, sp, #0x8 // x0 = sp + 0x8
    0x102f05dd0 <+24>: mov    x1, #0x0 // x1 = 0x0
    0x102f05dd4 <+28>: str    xzr, [sp, #0x8] // *(sp + 0x8) = 0 也便是刚写入的 xzr(8 个字节的 0)
->  0x102f05dd8 <+32>: bl     0x102f06374               ; symbol stub for: objc_storeStrong // 承受两个参数,x1 假如是 null 的话便是对 x0 置空
    0x102f05ddc <+36>: ldp    x29, x30, [sp, #0x20]
    0x102f05de0 <+40>: add    sp, sp, #0x30
    0x102f05de4 <+44>: ret    

哦豁,代码量一下子就提高了很多。但不要紧咱们只需求重视一个点即可,那便是尽管没有任何值赋给 __strong object,但其实 ARC 依旧帮咱们调用了 objc_storeStrong ,而且调用的仍是 objc_storeStrong(sp + 0x8, 0x0)。这个调用的意思便是对 sp + 0x8 这块内存置空。

// objc4-706
void
objc_storeStrong(id *location, id obj)
{
    id prev = *location;
    if (obj == prev) {
        return;
    }
    objc_retain(obj);
    *location = obj;
    objc_release(prev);
}

咱们能够对比下手动置 nil 的汇编代码,没有任何差异。

- (void)nsobjectPointer {
    NSObject *object = nil;
->  
}
(lldb) dis
demo`-[ViewController nsobjectPointer]:
    0x102e3ddb8 <+0>:  sub    sp, sp, #0x30
    0x102e3ddbc <+4>:  stp    x29, x30, [sp, #0x20]
    0x102e3ddc0 <+8>:  add    x29, sp, #0x20
    0x102e3ddc4 <+12>: stur   x0, [x29, #-0x8]
    0x102e3ddc8 <+16>: str    x1, [sp, #0x10]
    0x102e3ddcc <+20>: add    x0, sp, #0x8
    0x102e3ddd0 <+24>: mov    x1, #0x0
    0x102e3ddd4 <+28>: str    xzr, [sp, #0x8]
->  0x102e3ddd8 <+32>: bl     0x102e3e374               ; symbol stub for: objc_storeStrong
    0x102e3dddc <+36>: ldp    x29, x30, [sp, #0x20]
    0x102e3dde0 <+40>: add    sp, sp, #0x30
    0x102e3dde4 <+44>: ret    

咱们终究再看下关于 __weak 变量,成果是如何的。

- (void)nsobjectPointer {
    __weak NSObject *object = nil;
->  
}
(lldb) dis
demo`-[ViewController nsobjectPointer]:
    0x104c75db0 <+0>:  sub    sp, sp, #0x30
    0x104c75db4 <+4>:  stp    x29, x30, [sp, #0x20]
    0x104c75db8 <+8>:  add    x29, sp, #0x20
    0x104c75dbc <+12>: stur   x0, [x29, #-0x8]
    0x104c75dc0 <+16>: str    x1, [sp, #0x10]
    0x104c75dc4 <+20>: add    x0, sp, #0x8 // x0 = sp + 0x8
    0x104c75dc8 <+24>: str    xzr, [sp, #0x8] // *(sp + 0x8) = 0x0,这时分 x0 便是 __weak 的 object,也便是置空
->  0x104c75dcc <+28>: bl     0x104c76338               ; symbol stub for: objc_destroyWeak // 只接收了一个参数,x0,也便是把 sp + 0x8 这个地址传入,这个地址的值是 0 
    0x104c75dd0 <+32>: ldp    x29, x30, [sp, #0x20]
    0x104c75dd4 <+36>: add    sp, sp, #0x30
    0x104c75dd8 <+40>: ret    

因而咱们知道了在 ARC 下,把 __strong/ __weak 变量初始化为 nil 是编译器特性,因为编译器会帮咱们插入 objc_storeStrong/objc_retain 等函数来进行操控,在这个时分就会把 NSObject 类型的指针初始化为 0 啦。

详细能够检查:Clang – AutomaticReferenceCounting.html

It is undefined behavior if the storage of a __strong or __weak object is not properly initialized before the first managed operation is performed on the object, or if the storage of such an object is freed or reused before the object has been properly deinitialized. Storage for a __strong or __weak object may be properly initialized by filling it with the representation of a null pointer, e.g. by acquiring the memory with calloc or using bzero to zero it out. A __strong or __weak object may be properly deinitialized by assigning a null pointer into it. A __strong object may also be properly initialized by copying into it (e.g. with memcpy) the representation of a different __strong object whose storage has been properly initialized; doing this properly deinitializes the source object and causes its storage to no longer be properly initialized. A __weak object may not be representation-copied in this way.

这句话的了解是:类目标的所有 Property 默认值都是 nil,因为类目标是 calloc 出来 或许 经过 bzero 置零了。

// objc4-706
// Call [cls alloc] or [cls allocWithZone:nil], with appropriate 
// shortcutting optimizations.
static ALWAYS_INLINE id
callAlloc(Class cls, bool checkNil, bool allocWithZone=false)
{
    if (slowpath(checkNil && !cls)) return nil;
#if __OBJC2__
    if (fastpath(!cls->ISA()->hasCustomAWZ())) {
        // No alloc/allocWithZone implementation. Go straight to the allocator.
        // fixme store hasCustomAWZ in the non-meta class and 
        // add it to canAllocFast's summary
        if (fastpath(cls->canAllocFast())) {
            // No ctors, raw isa, etc. Go straight to the metal.
            bool dtor = cls->hasCxxDtor();
            id obj = (id)calloc(1, cls->bits.fastInstanceSize());
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            obj->initInstanceIsa(cls, dtor);
            return obj;
        }
        else {
            // Has ctor or raw isa or something. Use the slower path.
            id obj = class_createInstance(cls, 0);
            if (slowpath(!obj)) return callBadAllocHandler(cls);
            return obj;
        }
    }
#endif
    // No shortcuts available.
    if (allocWithZone) return [cls allocWithZone:nil];
    return [cls alloc];
}

终究,咱们把杂乱带回来值的这大段汇编的详细了解,就当留一个课后作业给咱们,感兴趣的同学能够自行剖析。

- (NSObject *)nsobjectPointer {
    NSObject *object;
->  return object;
}
(lldb) dis
demo`-[ViewController nsobjectPointer]:
    0x100df9ce8 <+0>:  sub    sp, sp, #0x40
    0x100df9cec <+4>:  stp    x29, x30, [sp, #0x30]
    0x100df9cf0 <+8>:  add    x29, sp, #0x30
    0x100df9cf4 <+12>: stur   x0, [x29, #-0x8]
    0x100df9cf8 <+16>: stur   x1, [x29, #-0x10]
    0x100df9cfc <+20>: add    x8, sp, #0x18 // x8 = sp + 0x18
    0x100df9d00 <+24>: str    x8, [sp, #0x8] // *(sp + 0x8) = x8
    0x100df9d04 <+28>: mov    x8, #0x0 // 清空 x8,这时分 x8 其实便是 object 了
    0x100df9d08 <+32>: str    x8, [sp] // *(sp) = x8 = 0 
    0x100df9d0c <+36>: str    xzr, [sp, #0x18] // *(sp + 0x18) = 0x0 (8 个字节的 0)
->  0x100df9d10 <+40>: ldr    x0, [sp, #0x18] // x0 = *(sp + 0x18) ,也便是刚写入的 xzr(8 个字节的 0)
    0x100df9d14 <+44>: bl     0x100dfa2bc               ; symbol stub for: objc_retain // objc_retain 只要一个参数,便是 x0,而上一步 x0 = *(sp + 0x18) ,也便是把 0 传入(啥都没干)
    0x100df9d18 <+48>: ldr    x1, [sp] // 之前 *sp 的方位是被写入了 0x0 的 0x100df9d08 <+32>
    0x100df9d1c <+52>: mov    x2, x0
    0x100df9d20 <+56>: ldr    x0, [sp, #0x8]
    0x100df9d24 <+60>: str    x2, [sp, #0x10]
    0x100df9d28 <+64>: bl     0x100dfa2d4               ; symbol stub for: objc_storeStrong // 承受两个参数,x1 假如是 null 的话便是对 x0 置空
    0x100df9d2c <+68>: ldr    x0, [sp, #0x10]
    0x100df9d30 <+72>: ldp    x29, x30, [sp, #0x30]
    0x100df9d34 <+76>: add    sp, sp, #0x40
    0x100df9d38 <+80>: b      0x100dfa28c               ; symbol stub for: objc_autoreleaseReturnValue

MRC

假如咱们用 MRC 编译文件,来看下对应的汇编代码,与之前的一般指针完全共同,因而在 MRC 状况下,定论与一般指针共同。

- (NSObject *)nsobjectPointer {
    NSObject *object;
    return object;
}
(lldb) dis
demo`-[ViewController nsobjectPointer]:
    0x1046a1d30 <+0>:  sub    sp, sp, #0x20
    0x1046a1d34 <+4>:  str    x0, [sp, #0x18]
    0x1046a1d38 <+8>:  str    x1, [sp, #0x10]
->  0x1046a1d3c <+12>: ldr    x0, [sp, #0x8]
    0x1046a1d40 <+16>: add    sp, sp, #0x20
    0x1046a1d44 <+20>: ret    

因而也就会产生溃散。

image

image

总结

// ARC 环境
- (void)sampleARC {
    NSObject *obj; // nil,值确认
    BOOL boolean; // 未定义,值不确认
    NSInteger inter; // 未定义,值不确认
    char *pointer; // 未定义,值不确认
    static BOOL staticBoolean; // false / 0,值确认
}
// MRC 环境
- (void)sampleMRC {
    NSObject *obj; // 未定义,值不确认
}

本文经过了解 ARM64 汇编的办法验证了下面的这些状况:

  1. 关于 C 的基础类型,假如未初始化:

    1. 部分变量:值不确认,需求初始化。
    2. 大局变量或许静态变量:值确认为 NULL 或许 0,原因是这部分储存在 __bss/__common 段内存会被初始化为 0。
  2. 指针假如未初始化:

    1. ARC 状况下,OC 目标指针:因为编译器生成代码帮忙,值确认为 nil 。
    2. MRC 状况下,OC 目标指针:值不确认,需求初始化。
    3. 非 OC 目标指针:值不确认,需求初始化。

前置常识

C的内存布局

image

堆内存 (stack) 与栈内存 (heap)

- (void)memberFunction {
    BOOL memberArithmetic = NO; // 栈
    NSObject *pointer = [[NSObject alloc] init]; // pointer 自身是在栈上,但 *pointer (也便是分配的 NSObject)在堆上
}

关于 OC 来说因为机制的原因,假如是 new 或许 alloc 出来的目标都是 堆 (heap) 内存,而一些结构体或许基础类型,便是 栈 (stack) 内存。假如是 C++ 的话,就比较杂乱了,目标能够创建在堆上,也能够创建在栈上,看创建办法。

假如不熟练的话,能够经过运转时的指令进行 check,对比地址与 fp 与 sp。

(lldb)p $fp >= &(memberArithmetic) && &(memberArithmetic) >= $sp // memberArithmetic 在栈上
(bool) $0 = true
(lldb) p $fp >= &(pointer) && &(pointer) >= $sp // pointer 自身是在栈上
(bool) $1 = true
(lldb) p $fp >= (pointer) && (pointer) >= $sp // pointer (也便是分配的 NSObject)在堆上
(bool) $2 = false

分配在栈上的变量的地址一定是处于 fp(frame pointer / x29 / w29) 与 sp(stack pointer) 之间的。里边的变量能够自己进行替换。

而 栈/堆 内存是由体系分配的虚拟内存(Virtual Memory),实践终究会映射到物理内存,物理内存或许是之前其他进程使用过,也或许是当时进程使用过的,总之拿到这块内存的时分,里边的值不一定都是经过初始化为 0 的。甚至有些 malloc() 的完成会特意在 debug 状态下赋值为非零以充沛露出问题。

The operating system does not guarantee a zero’ed out memory, just that you own it. It will probably give you pages of memory that were used before (or never used before, but non-zero). If an application stores potentially-sensitive data, it is expected to zero it before free()’ing.

stackoverflow.com/questions/5…

When the OS has to apportion a new page to your process (whether that’s for its stack or for the arena used by malloc()), it guarantees that it won’t expose data from other processes; the usual way to ensure that is to fill it with zeros (but it’s equally valid to overwrite with anything else, including even a page worth of /dev/urandom- in fact some debugging malloc() implementations write non-zero patterns, to catch mistaken assumptions such as yours).

unix.stackexchange.com/questions/5…

BSS 段

BSS (Block Starting by Symbol ),寄存未初始化的 statically allocated objects。statically allocated objects 一起指代:

特别注意:详细是否分配在 BSS 段是取决于 编译器以及详细的参数的,例如 llvm 中就还有 __common 段(紧跟着 __bss段 ),部分状况也有所不同。这儿的解说主要以 wikipedia 为主,wiki 中较多仍是以 GCC 角度解说。实践变量存在哪个段中还需求实践检查编译产品。

  1. 未初始化的 大局变量/常量 。(这一条 GCC 与 LLVM 不共同)

all uninitialized objects (both variables and constants) declared at file scope (i.e., outside any function)

A global variable is a variable that is defined outside all functions and available to all functions.

BOOL unintializedGlobalVariable;
const BOOL unintializedGlobalConstant;
- (void)function {}
  1. 未初始化的部分静态变量

uninitialized static local variables (local variables declared with the static keyword);

- (void)function {
    static BOOL unintializedLocalStaticVariables;
}

但终究,还有一类变量也或许是在 BSS 端中:初始化为 0 的 大局变量或静态变量。当然这个是看编译器支撑的,不是一向而论的。例如 GCC 的 -fzero-initialized-in-bss 与 -fno-zero-initialized-in-bss 就能操控这个特性。

statically-allocated variables and constants initialized with a value consisting solely of zero-valued bits

int intializedGlobalVariable = 0;
- (void)function {}

BSS 段的特征是当程序开始运转时,全部都是 0 (由 OS 内核确保),因而假如是 基础特点,那便是 0 ,假如是指针,那便是 空指针。实践因为未初始化,因而在 .o 文件中不占用实践空间,仅仅一个占位 (placeholder),也因而也能够解说为 ‘Better Save Space’ 。

经过产品检查

说了这么多或许被绕晕了,whatever ,咱们能够直接在产品中 grep 检查对应成果来看到底是不是在 bss 段中。我的编译环境为 Xcode 14.2 (LLVM)。

BOOL unintializedGlobalVariable;
const BOOL unintializedGlobalConstant;
int intializedGlobalVariable = 0;
const BOOL intializedGlobalConstant = YES;
- (void)viewDidLoad {
    [super viewDidLoad];
    static BOOL unintializedLocalStaticVariable;
    static const BOOL unintializedLocalStaticConstant;
    static BOOL intializedLocalStaticVariable = YES;
}
➜  arm64 objdump -t ViewController.o | grep intialized
0000000000001175 l     O __DATA,__bss _viewDidLoad.unintializedLocalStaticVariable
0000000000000145 l     O __TEXT,__const _viewDidLoad.unintializedLocalStaticConstant
00000000000001c8 l     O __DATA,__data _viewDidLoad.intializedLocalStaticVariable
0000000000000144 g     O __TEXT,__const _intializedGlobalConstant
0000000000001170 g     O __DATA,__common _intializedGlobalVariable
0000000000000146 g     O __TEXT,__const _unintializedGlobalConstant
0000000000001174 g     O __DATA,__common _unintializedGlobalVariable

咱们能够看到跟 wiki 上的介绍有些出入,例如 所有常量都在 __TEXT,__const 区;一起 LLVM 把 大局变量(GlobalVariable) 统统放到了 DATA,__common 区;未初始化的部分静态变量在 bss 区。

关于这个输出的详细参数能够检查:objdump 的 -t/–syms 部分。

When your program starts running, all the contents of the bss section are zeroed bytes.

web.mit.edu/rhel-doc/3/…

Hence, the BSS segment typically includes all uninitialized objects (both variables and constants) declared at file scope (i.e., outside any function) as well as uninitialized static local variables (local variables declared with the static keyword);

An implementation may also assign statically-allocated variables and constants initialized with a value consisting solely of zero-valued bits to the BSS section.

en.wikipedia.org/wiki/.bss#B…

If a global variable is explicitly initialized to zero (int myglobal = 0), where that variable will be stored?Compiler is free to put such variable into bss as well as into data.

stackoverflow.com/questions/8…

Data in this segment is initialized by the kernel to arithmetic 0 before the program starts executing;contains all global variables and static variables that are initialized to zero or do not have explicit initialization in source code.

www.geeksforgeeks.org/memory-layo…

尽管这儿没有十分显着的依据,但咱们仍是能够以为__common段和__bss段没有太大差异;而之所以__common段独立于__bss段,是因为要考虑到 大局变量需求露出给外部(external) ,涉及到“弱符号与强符号”的问题(这儿不作介绍),否则与__bss段没差异。

C-C-中已初始化-未初始化大局-静态-部分变量-常量在内存中的方位

关于这部分内容,详细能够检查:Memory Layout of C Programs 一文。当然实践关于内存的了解还有十分多的内容,例如 page in/out、触发中止、clean/dirty memory 等等,跟本文无关咱们就不再展开了。

ARM64 汇编

这是一个比较大的论题,咱们在这儿只解说必要的指令协助咱们了解,其他杂乱的指令咱们假如作业顶用得到的话再学习即可。

sub / add

意思便是简略的加减法,关于 CPU 来说,能直接操作的只要寄存器,不能直接操作内存,因而一般参数都是寄存器。

sub sp, sp, #0x20 的意思是 sp = sp – 0x20 。因为 stack 内存是由高地址向地址分配,因而对 sp 进行 sub 操作本质上 push stack 的行为。

add sp, sp, #0x20 的意思是 sp = sp + 0x20 。本质上是 pop stack 的行为,一般来说同一个函数调用中,push 跟 pop 的偏移量是保持共同的。

str / ldr(ldrb)

这是一个对应的操作,str (store register)是把寄存器的值写到内存里,ldr (load register) 是把内存的值读到寄存器里。

str x0, sp 的意思便是把 sp 的写到 x0 里边。能够了解为 sp = xo 。一般呈现于调用 C 函数且第一个参数是指针的状况。

str x0, [sp, #0x18] 的意思是把 sp + 0x18 这个地址的内容存到 x0 里边。[] 符号能够了解为 C 语言的 * 符号,能够了解为 *(sp + 0x18) = x0。

ldr x0, sp 的意思便是把 sp 的读到 x0 里边。能够了解为 x0 = sp。

ldr x8, [sp, #0xf] 综上,意思便是把 sp + 0xf 这个地址的内容存到 x8 里边。能够了解为 x8 = *(sp + 0xf) 。

ldrb w1, [sp, #0xf] ,与 ldr 大致相同,差异是多了一个 b(byte),表示只读一个字节,然后用 0 填充到高位,直到满足 4 byte (也便是 w1 的大小)。w1 是拜访时是寄存器的 低 32 位(4 byte),x1 拜访时是寄存器 R1(Register 1) 的完好的 64 位(8 byte)。

image

引用

  1. How is the stack initialized?

  2. If the heap is zero-initialized for security, then why is the stack merely uninitialized?

  3. objdump(1) — Linux manual page

  4. 5.5. bss Section

  5. .bss

  6. If a global variable is initialized to 0, will it go to BSS?

  7. C-C-中已初始化-未初始化大局-静态-部分变量-常量在内存中的方位

  8. “ISO/IEC 9899:TC3 (Current C standard)”

  9. Static storage union and named members initialization in C language

  10. Uninitialized/ variable

  11. Clang AVAILABLE CHECKERS

  12. ARC