窥探字符串的内存
- 首先 ,我们定义一个短的字符串,如下所示,通过
MemoryLayout
打印,我们知道了str1变量
占用了16个字节,那么这16个字节究竟存储了什么呢 ,我们通过汇编来看一下
- 首先 ,我们定义一个短的字符串,如下所示,通过
var str1 = "0123456789"
print(MemoryLayout.stride(ofValue: str1)) 打印出来是16个字节,也就是说str1占用了16字节
- 为了防止干扰,我们把打印的代码注释掉,只留下
var str1 = "0123456789"
,以下就是这句代码的汇编代码
- 为了防止干扰,我们把打印的代码注释掉,只留下
TestSwift`main:
0x100001390 <+0>: pushq %rbp
0x100001391 <+1>: movq %rsp, %rbp
0x100001394 <+4>: subq $0x10, %rsp
-> 0x100001398 <+8>: leaq 0x4361(%rip), %rax ; "0123456789"
0x10000139f <+15>: movl %edi, -0x4(%rbp)
0x1000013a2 <+18>: movq %rax, %rdi
0x1000013a5 <+21>: movl $0xa, %eax
0x1000013aa <+26>: movq %rsi, -0x10(%rbp)
0x1000013ae <+30>: movq %rax, %rsi
0x1000013b1 <+33>: movl $0x1, %edx
0x1000013b6 <+38>: callq 0x100005402 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String
0x1000013bb <+43>: xorl %ecx, %ecx
0x1000013bd <+45>: movq %rax, 0x5e24(%rip) ; TestSwift.str1 : Swift.String
0x1000013c4 <+52>: movq %rdx, 0x5e25(%rip) ; TestSwift.str1 : Swift.String + 8
0x1000013cb <+59>: movl %ecx, %eax
0x1000013cd <+61>: addq $0x10, %rsp
0x1000013d1 <+65>: popq %rbp
0x1000013d2 <+66>: retq
- 我们重点观察一下下面两句汇编,通过注释和操作数
q
(q代表操作8个字节的空间),就可以看出来第一句是操作了str1变量
的前8个字节,第二句操作了str1变量
的后8个字节
- 我们重点观察一下下面两句汇编,通过注释和操作数
0x1000013bd <+45>: movq %rax, 0x5e24(%rip) ; TestSwift.str1 : Swift.String
0x1000013c4 <+52>: movq %rdx, 0x5e25(%rip) ; TestSwift.str1 : Swift.String + 8
-
0x5e24(%rip)
的意思就是将rip寄存器
的值与0x5e24
相加 ,我们知道rip寄存器
存储着下一条指令的地址,也就是0x1000013c4
,与0x5e24
相加,就是0x1000071E8
,通过LLDB命令x/2xg
,打印出从0x1000071E8
地址开始的16个字节的数据,来进行观察,如下所示
-
(lldb) x/2xg 0x1000071E8
0x1000071e8: 0x3736353433323130 0xea00000000003938
上面是小端模式读取的,不好看,现在用另一种模式读取,就很容易看出来
(lldb) x 0x1000071E8
0x1000071e8: 30 31 32 33 34 35 36 37 38 39 00 00 00 00 00 ea 0123456789......
0x1000071f8: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
- 我们发现
str1变量
中存储的数据就是字符串"0123456789"
的ASCII值的十六进制,也就是说短的字符串,数据直接会存放在变量的内存里面(非常类似OC中的tagger pointer)。
- 我们发现
- 我们逐渐加长字符串并且一一查看内存,如下所示,就可以看出来,当字符串的长度
小于16
时,随着字符串长度的逐渐增加,最后一个字节存放着0xe
不变,0xa
逐渐增大到了0xf
,也就说0xe
很有可能代表字符串的类型,而0xa
肯定就是字符串的长度;当字符串的长度大于等于16
时,str1变量
的内存突然就发生了变化,那么此时字符串是怎么存储的呢?
- 我们逐渐加长字符串并且一一查看内存,如下所示,就可以看出来,当字符串的长度
var str1 = "0123456789"时, str1的内存是:0x3736353433323130 0xea00000000003938
var str1 = "0123456789A"时, str1的内存是:0x3736353433323130 0xeb00000000413938
var str1 = "0123456789AB"时, str1的内存是:0x3736353433323130 0xec00000042413938
var str1 = "0123456789ABC"时, str1的内存是:0x3736353433323130 0xed00004342413938
var str1 = "0123456789ABCD"时, str1的内存是:0x3736353433323130 0xee00444342413938
var str1 = "0123456789ABCDE"时, str1的内存是:0x3736353433323130 0xef45444342413938
var str1 = "0123456789ABCDEF"时,str1的内存是:0xd000000000000010 0x80000001000056d0
- 我们来分析一下
var str1 = "0123456789ABCDEF"
的汇编代码,窥探一下字符串是怎么存储的,下面就是这句代码的汇编代码
- 我们来分析一下
TestSwift`main:
0x100001380 <+0>: pushq %rbp
0x100001381 <+1>: movq %rsp, %rbp
0x100001384 <+4>: subq $0x10, %rsp
-> 0x100001388 <+8>: leaq 0x4361(%rip), %rax ; "0123456789ABCDEF"
0x10000138f <+15>: movl %edi, -0x4(%rbp)
0x100001392 <+18>: movq %rax, %rdi
0x100001395 <+21>: movl $0x10, %eax
0x10000139a <+26>: movq %rsi, -0x10(%rbp)
0x10000139e <+30>: movq %rax, %rsi
0x1000013a1 <+33>: movl $0x1, %edx
0x1000013a6 <+38>: callq 0x1000053f2 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String
0x1000013ab <+43>: xorl %ecx, %ecx
0x1000013ad <+45>: movq %rax, 0x5e34(%rip) ; TestSwift.str1 : Swift.String
0x1000013b4 <+52>: movq %rdx, 0x5e35(%rip) ; TestSwift.str1 : Swift.String + 8
0x1000013bb <+59>: movl %ecx, %eax
0x1000013bd <+61>: addq $0x10, %rsp
0x1000013c1 <+65>: popq %rbp
0x1000013c2 <+66>: retq
- 重点观察这几句汇编,我们来一句一句分析一下,如下所示
leaq指令是直接赋值地址,从注释可以看出来是把"0123456789ABCDEF"的真实地址给了rax寄存器
通过rip+0x4361,可以算出字符串的真实地址是:0x1000056F0
0x100001388 <+8>: leaq 0x4361(%rip), %rax ; "0123456789ABCDEF"
又把rax寄存器的值给了rdi寄存器,也就是说rdi寄存器里存放着字符串的真实地址
0x100001392 <+18>: movq %rax, %rdi
把0x10给了eax寄存器,eax寄存器就是rax寄存器,也就是说rax寄存器里存储着字符串的长度(十六进制0x10就是十进制的16)
0x100001395 <+21>: movl $0x10, %eax
又把rax寄存器的值给了rsi寄存器,也就是说rsi寄存器里的值就是字符串的长度
0x10000139e <+30>: movq %rax, %rsi
调用了函数String.init(),把rdi寄存器和rsi寄存器作为了参数,也就说把字符串的真实地址和字符串长度作为参数,调用了String.init()
0x1000013a6 <+38>: callq 0x1000053f2 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String
把rax寄存器的值给了str1变量的前8个字节
0x1000013ad <+45>: movq %rax, 0x5e34(%rip) ; TestSwift.str1 : Swift.String
把rdx寄存器的值给了str1变量的后8个字节
0x1000013b4 <+52>: movq %rdx, 0x5e35(%rip) ; TestSwift.str1 : Swift.String + 8
- 从上面分析可以看出来,
rdi寄存器
存放着字符串的真实地址,rsi寄存器
存放着字符串的长度,然后又把这两个作为参数,调用了String.init()函数
,最后函数的把返回值存在了rax寄存器
和rdx寄存器
中,又分别放到了str1变量
的前8个字节和后8个字节中,那么我们再来分析一下String.init()函数
内部究竟做了什么,我们来看一下String.init()函数
的汇编代码:
- 从上面分析可以看出来,
libswiftCore.dylib`Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String:
-> 0x7fff722e9c40 <+0>: pushq %rbp
0x7fff722e9c41 <+1>: movq %rsp, %rbp
0x7fff722e9c44 <+4>: pushq %r14
0x7fff722e9c46 <+6>: pushq %rbx
0x7fff722e9c47 <+7>: subq $0x10, %rsp
0x7fff722e9c4b <+11>: testq %rsi, %rsi
0x7fff722e9c4e <+14>: js 0x7fff722e9e31 ; <+497>
0x7fff722e9c54 <+20>: movl %edx, %eax
0x7fff722e9c56 <+22>: movabsq $-0x2000000000000000, %rdx ; imm = 0xE000000000000000
0x7fff722e9c60 <+32>: testq %rsi, %rsi
0x7fff722e9c63 <+35>: je 0x7fff722e9ca4 ; <+100>
0x7fff722e9c65 <+37>: cmpq $0xf, %rsi
0x7fff722e9c69 <+41>: jle 0x7fff722e9cab ; <+107>
0x7fff722e9c6b <+43>: movabsq $-0x4000000000000000, %rcx ; imm = 0xC000000000000000
0x7fff722e9c75 <+53>: orq %rsi, %rcx
0x7fff722e9c78 <+56>: testb $0x1, %al
0x7fff722e9c7a <+58>: cmoveq %rsi, %rcx
0x7fff722e9c7e <+62>: movabsq $0x1000000000000000, %rax ; imm = 0x1000000000000000
0x7fff722e9c88 <+72>: orq %rcx, %rax
0x7fff722e9c8b <+75>: movabsq $0x7fffffffffffffe0, %rdx ; imm = 0x7FFFFFFFFFFFFFE0
0x7fff722e9c95 <+85>: addq %rdx, %rdi
0x7fff722e9c98 <+88>: addq $0x20, %rdx
- 我们重点看下面三句汇编,在
String.init()函数
内部,它比较了0xf
与rsi寄存器
值的大小,前面说过rsi寄存器
中存放着字符串的长度,也就是比较了0xf
与字符串的长度;又把rdx寄存器
的值变成了字符串的真实地址+$0x7fffffffffffffe0
,从上面的分析我们知道rdx寄存器
的值最后又给了str1变量
的后8个字节,也就是说现在str1变量
的后8个字节存放的是字符串的真实地址+$0x7fffffffffffffe0
- 我们重点看下面三句汇编,在
比较0xf与rsi寄存器值的大小,前面说过rsi寄存器中存放着字符串的长度
0x7fff722e9c65 <+37>: cmpq $0xf, %rsi
将立即数$0x7fffffffffffffe0放到rdx寄存器中
0x7fff722e9c8b <+75>: movabsq $0x7fffffffffffffe0, %rdx ; imm = 0x7FFFFFFFFFFFFFE0
将rdx寄存器中的值与rdi寄存器相加,并且放到rdx寄存器中,前面说过rdi寄存器放着字符串的真实地址
所以现在rdx寄存器中放着字符串的真实地址+$0x7fffffffffffffe0
0x7fff722e9c95 <+85>: addq %rdx, %rdi
- 通过以下两种方式都可以算出来字符串的真实地址是
0x1000056F0
,那么这个地址究竟指向内存空间的哪里呢,我们通过MacOView工具来查看一下这个地址
- 通过以下两种方式都可以算出来字符串的真实地址是
通过rip+0x4361,可以算出字符串的真实地址是:0x1000056F0
0x100001388 <+8>: leaq 0x4361(%rip), %rax ; "0123456789ABCDEF"
也可以通过str1后8个字节的数据 - 0x7fffffffffffffe0 算出来,字符串的真实地址是0x1000056F0
var str1 = "0123456789ABCDEF"时,str1的内存是:0xd000000000000010 0x80000001000056d0
- MacOView中的地址是虚拟地址,需要加上
0x1000000
才是内存中的真实地址,也就是字符串的真实地址0x1000056F0
,减去0x1000000
,算出来的0x56F0
才是在MacOView中的虚拟地址,从这个0x56F0
地址存放的位置,从下图可以看出,字符串在_TEXT,_cstring
中,也就是常量区,所以得出结论:当字符串的长度大于等于16
时,字符串会存储在常量区
。
- MacOView中的地址是虚拟地址,需要加上
- 其实无论字符串长短,
初始化时的字符串都会在常量区
,当字符串长度小于16时,会把常量区的字符串直接放到变量的内存中;当字符串长度大于等于16时,会把常量区的地址加上某个立即数,然后放在变量的后8个字节中。
- 其实无论字符串长短,
- 我们知道
当程序运行时,常量区的值就不能更改了
,那么当我们拼接字符串时,字符串又是如何存储的呢?来看看下面的代码
- 我们知道
var str1 = "0123456789ABCDEF"
str1.append("G")
- 这两句代码的汇编是这样的 :
TestSwift`main:
0x100001300 <+0>: pushq %rbp
0x100001301 <+1>: movq %rsp, %rbp
0x100001304 <+4>: pushq %r13
0x100001306 <+6>: subq $0x38, %rsp
-> 0x10000130a <+10>: leaq 0x43df(%rip), %rax ; "0123456789ABCDEF"
0x100001311 <+17>: movl %edi, -0x24(%rbp)
0x100001314 <+20>: movq %rax, %rdi
0x100001317 <+23>: movl $0x10, %eax
0x10000131c <+28>: movq %rsi, -0x30(%rbp)
0x100001320 <+32>: movq %rax, %rsi
0x100001323 <+35>: movl $0x1, %edx
0x100001328 <+40>: callq 0x1000053d2 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String
0x10000132d <+45>: movq %rax, 0x5ec4(%rip) ; TestSwift.str1 : Swift.String
0x100001334 <+52>: movq %rdx, 0x5ec5(%rip) ; TestSwift.str1 : Swift.String + 8
0x10000133b <+59>: leaq 0x43bf(%rip), %rdi ; "'G'"
0x100001342 <+66>: movl $0x1, %esi
0x100001347 <+71>: movl $0x1, %edx
0x10000134c <+76>: callq 0x1000053d2 ; symbol stub for: Swift.String.init(_builtinStringLiteral: Builtin.RawPointer, utf8CodeUnitCount: Builtin.Word, isASCII: Builtin.Int1) -> Swift.String
0x100001351 <+81>: leaq 0x5ea0(%rip), %rsi ; TestEnumMemory.str1 : Swift.String
0x100001358 <+88>: xorl %ecx, %ecx
0x10000135a <+90>: movq %rsi, %rdi
0x10000135d <+93>: leaq -0x20(%rbp), %rsi
0x100001361 <+97>: movl $0x21, %r8d
0x100001367 <+103>: movq %rdx, -0x38(%rbp)
0x10000136b <+107>: movq %r8, %rdx
0x10000136e <+110>: movq %rax, -0x40(%rbp)
0x100001372 <+114>: callq 0x10000547a ; symbol stub for: swift_beginAccess
0x100001377 <+119>: movq -0x40(%rbp), %rdi
0x10000137b <+123>: movq -0x38(%rbp), %rsi
0x10000137f <+127>: leaq 0x5e72(%rip), %r13 ; TestSwift.str1 : Swift.String
0x100001386 <+134>: callq 0x1000053d8 ; symbol stub for: Swift.String.append(Swift.String) -> ()
0x10000138b <+139>: leaq -0x20(%rbp), %rdi
0x10000138f <+143>: callq 0x100005498 ; symbol stub for: swift_endAccess
0x100001394 <+148>: movq -0x38(%rbp), %rdi
0x100001398 <+152>: callq 0x100005480 ; symbol stub for: swift_bridgeObjectRelease
0x10000139d <+157>: xorl %eax, %eax
0x10000139f <+159>: addq $0x38, %rsp
0x1000013a3 <+163>: popq %r13
0x1000013a5 <+165>: popq %rbp
0x1000013a6 <+166>: retq
- 重点观察这一句
0x10000137f <+127>: leaq 0x5e72(%rip), %r13 ; TestEnumMemory.str1 : Swift.String
,str1变量
的地址时在r13寄存器
中的,我们打印r13寄存器
中的值,就可以得出str1变量
的地址,读取str1变量
的后 8个字节,我们发现从第33个字节开始,就是我们存储的字符串了,有经验的话,一眼就可以看出,str1变量的后8个字节
,存放的是堆空间的地址
。
- 重点观察这一句
str1变量的地址从0x00000001000071f8开始的
(lldb) register read r13
r13 = 0x00000001000071f8 TestEnumMemory`TestEnumMemory.str1 : Swift.String
从0x00000001000071f8开始,读取16个字节
(lldb) x/2xg 0x00000001000071f8
0x1000071f8: 0xf000000000000011 0x000000010070ac40
读取 str1变量的后8个字节
(lldb) x/10xg 0x000000010070ac40
0x10070ac40: 0x00007fff9cc0fca8 0x0000000000000002
0x10070ac50: 0x0000000000000018 0xf000000000000011
0x10070ac60: 0x3736353433323130 0x4645444342413938
0x10070ac70: 0x00007fff93e30047 0x0000000000000000
0x10070ac80: 0x0000000000000000 0x0000000000000000
- 这里总结一下字符串的内存
如果一开始初始化时,字符串长度
小于16时
,会直接把常量区字符串的内容 ,拷贝到str1变量的内存中,例如:var str1 = "0123456789"
,字符串的内容就以ASCII的形式存储在str1变量的内存中如果一开始初始化时,字符串长度
大于等于16时
,字符串内容会存放在常量区(__TEXT.cstring),变量的前8个字节存放标识符和字符串长度
,会把字符串的常量区的真实地址加上某个立即数
,存放在变量的后8个字节中,例如:var str1 = "0123456789ABCDEF"
,str1变量的前8个字节存放标识符和字符串长度,后8个字节就存放着字符串的常量区地址+某个立即数
如果字符串拼接之后如果仍然
小于16时
,字符串的内容还是存放在str1变量的内存中,例如:var str1 = "012345"; str1.append("ABCDE")
,拼接后字符串的内容仍然在str1变量的内存中如果字符串拼接之后长度
大于等于16
,会开辟堆空间
,变量的后8个字节存放着这个堆空间的地址信息
,堆空间的前32个字节存放描述信息,后面才是真正的字符串内容,例如:var str1 = "012345678"; str1.append("ABCDEFGHEFSJ")
,拼接之后,就会开辟堆空间,str1变量的 后8个字节就是这个堆空间的地址,堆空间里的前32个字节存放描述信息,往后就是字符串的内容