汇编窥探Swift底层(五):字符串

窥探字符串的内存

    1. 首先 ,我们定义一个短的字符串,如下所示,通过MemoryLayout打印,我们知道了str1变量占用了16个字节,那么这16个字节究竟存储了什么呢 ,我们通过汇编来看一下
var str1 = "0123456789"
print(MemoryLayout.stride(ofValue: str1)) 打印出来是16个字节,也就是说str1占用了16字节
    1. 为了防止干扰,我们把打印的代码注释掉,只留下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   

    1. 我们重点观察一下下面两句汇编,通过注释和操作数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
    1. 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  ................

    1. 我们发现str1变量中存储的数据就是字符串"0123456789"ASCII值的十六进制,也就是说短的字符串,数据直接会存放在变量的内存里面(非常类似OC中的tagger pointer)。
    1. 我们逐渐加长字符串并且一一查看内存,如下所示,就可以看出来,当字符串的长度小于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
    1. 我们来分析一下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   

    1. 重点观察这几句汇编,我们来一句一句分析一下,如下所示
    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
 
    1. 从上面分析可以看出来,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
    1. 我们重点看下面三句汇编,在String.init()函数内部,它比较了0xfrsi寄存器值的大小,前面说过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
    1. 通过以下两种方式都可以算出来字符串的真实地址是0x1000056F0,那么这个地址究竟指向内存空间的哪里呢,我们通过MacOView工具来查看一下这个地址
   通过rip+0x4361,可以算出字符串的真实地址是:0x1000056F0
   0x100001388 <+8>:  leaq   0x4361(%rip), %rax        ; "0123456789ABCDEF"

   也可以通过str1后8个字节的数据  - 0x7fffffffffffffe0 算出来,字符串的真实地址是0x1000056F0
   var str1 = "0123456789ABCDEF"时,str1的内存是:0xd000000000000010 0x80000001000056d0
    1. MacOView中的地址是虚拟地址,需要加上0x1000000才是内存中的真实地址,也就是字符串的真实地址0x1000056F0,减去0x1000000,算出来的0x56F0才是在MacOView中的虚拟地址,从这个0x56F0地址存放的位置,从下图可以看出,字符串在_TEXT,_cstring中,也就是常量区,所以得出结论:当字符串的长度大于等于16时,字符串会存储在常量区
      0x56F0的位置
    1. 其实无论字符串长短,初始化时的字符串都会在常量区,当字符串长度小于16时,会把常量区的字符串直接放到变量的内存中;当字符串长度大于等于16时,会把常量区的地址加上某个立即数,然后放在变量的后8个字节中。
    1. 我们知道当程序运行时,常量区的值就不能更改了,那么当我们拼接字符串时,字符串又是如何存储的呢?来看看下面的代码
var str1 = "0123456789ABCDEF"
str1.append("G")
    1. 这两句代码的汇编是这样的 :
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   
    1. 重点观察这一句0x10000137f <+127>: leaq 0x5e72(%rip), %r13 ; TestEnumMemory.str1 : Swift.Stringstr1变量的地址时在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
    1. 这里总结一下字符串的内存
  • 如果一开始初始化时,字符串长度小于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个字节存放描述信息,往后就是字符串的内容

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,013评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,205评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,370评论 0 342
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,168评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,153评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,954评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,271评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,916评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,382评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,877评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,989评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,624评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,209评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,199评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,418评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,401评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,700评论 2 345