• 汇编分析Swift中的String


     

    关于String的思考 

    1、1个String变量占用多少内存?

    2、下面2个String变量,底层存储有什么不同?

    1. var str1 = "0123456789"
    2. var str2 = "0123456789ABCDEF"

    意思就是这两个字符串在内存中分别是存储在哪个区域的。

       内存

       地址

       从低

       到高

    代码区
    常量区
    全局区(数据段)
    堆空间
    栈空间
    动态库

    3、如果对String进行拼接操作,String变量的存储会发生什么变化?

    1. str1.append("ABCDE")
    2. str1.append("F")
    3. str2.append("G")

    4、ASCII码表:https://www.ascii-code.com/

    汇编分析String

     

    str1最少是有16个字节的,从8、9行就可以看出,通过MemoryLayout.stride()打印也可以看出来。

    在第10行处打断点,进行打印可以获取str1变量16个字节存储的东西:

    1. (lldb) register read rax
    2. rax = 0x3736353433323130
    3. (lldb) register read rdx
    4. rdx = 0xea00000000003938
    5. (lldb)

    rip的地址加上0x409f,也就是0x100003f71 +  0x409f = 0x100008010,这个就是str1的内存地址,通过以下命令可以获取该地址存储的内容:

    1. (lldb) x/2xg 0x100008010
    2. 0x100008010: 0x3736353433323130 0xea00000000003938
    3. (lldb)

    发现和上面打印出来的内容是相同的,所以我们从各个方面证明了str1变量存储的内容就是 0x3736353433323130 0xea00000000003938,占16个字节。

     

    以上是ASCII码值表,0对应16进制是0x30,1对应0x31,对应着看str1存储的内容,可以发现是从30一直到39的,相当于字符串的内容就直接放到了str1的内存当中了。

    我们换种打印方式可以看的更加清晰:

    1. (lldb) x 0x100008010
    2. 0x100008010: 30 31 32 33 34 35 36 37 38 39 00 00 00 00 00 ea 0123456789......
    3. 0x100008020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
    4. (lldb)

     0xea又代表什么内容呢?

    我们把9删掉:

    var str1 = "012345678"

    根据同样的方式获取到str1存储的内容:

    1. (lldb) register read rax
    2. rax = 0x3736353433323130
    3. (lldb) register read rdx
    4. rdx = 0xe900000000000038
    5. (lldb)

    可以发现0xea变成了0xe9,所以a\9这一位是用来存储字符串长度的,0xe表示的是字符串的存储方式,a\9这一位最大是f,f的时候就是刚刚好填满15个字节,也就是最多存储15个字符。

    我们可以来测试一下:

    var str1 = "0123456789ABCDE"
    1. (lldb) register read rax
    2. rax = 0x3736353433323130
    3. (lldb) register read rdx
    4. rdx = 0xef45444342413938
    5. (lldb)

     再看一下ASCII码表:

    发现确实是和我们的结论一致的。

    小于等于15位的时候,是直接存储在字符串变量的内存中的,类似于OC的tagger pointer。

    如果再多一位会发生什么事情呢?

    var str2 = "0123456789ABCDEF"
    1. (lldb) register read rax
    2. rax = 0xd000000000000010
    3. (lldb) register read rdx
    4. rdx = 0x8000000100003f70
    5. (lldb)

    我们可以再次尝试,从而找到规律:

    var str2 = "0123456789ABCDEFFDSFSDFDSF"
    1. (lldb) register read rax
    2. rax = 0xd00000000000001a
    3. (lldb) register read rdx
    4. rdx = 0x8000000100003f70
    5. (lldb)

    两次存储内容可以对比一下,其实变化很小,也可以间接证明,字符串内容不是存储在这16个字节里面的。

    那到底存储在什么地方呢?

     我们再来分析汇编:

     

     

     rax和rdx是str2存储内容,rax和rdx是callq返回的内容,超过八个字节放到rdx里面,我们可以跟进callq方法里面去看一下做了哪些事情。

    通过si我们可以进到Swift.String.init方法里面,第12行cmpq   $0xf, %rsi,就是将rsi和15进行比较,rsi就是要初始化的字符串的长度,第13行jle    0x7ff825d89c47,是根据比较结果进行跳转,如果小于就跳转到别的方法,其他情况就继续往下走。

    第20行movabsq $0x7fffffffffffffe0, %rdx,将0x7fffffffffffffe0放到rdx中,第21行addq   %rdx, %rdi,rdi就是字符串的真实地址,rdi加上rdx的值再放到rdx中。

    现在str2中的16个字节存储的是:

    1. (lldb) register read rdx
    2. rdx = 0x8000000100003f60
    3. (lldb) register read rax
    4. rax = 0xd000000000000010
    5. (lldb)

    字符串的真正内容是和0x8000000100003f60这8个字节相关的,通过汇编代码我们可以知道,

    字符串的真实地址 + 0x7fffffffffffffe0 = 0x8000000100003f60,那么可以推导出

    字符串的真实地址 = 0x8000000100003f60 - 0x7fffffffffffffe0

    经过计算0x100003F80就是字符串的真实地址。我们打印一下这个地址存储的内容:

    1. (lldb) x 0x100003F80
    2. 0x100003f80: 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 0123456789ABCDEF
    3. 0x100003f90: 00 31 00 0a 00 20 00 00 e8 fd ff ff 03 00 00 00 .1... ..........
    4. (lldb)

    发现确实是字符串的内容。

    rsi和rdi是如何确定是位数和真实地址的?

    再来一次汇编:

     

    第5行leaq   0x1f1(%rip), %rdi,将0x1f1(%rip)地址值赋值给rdi,可以通过注释看出0x1f1(%rip)地址值就是字符串的真实地址值,rip + 0x1f1 = 0x100003d8f + 0x1f1 = 0x100003F80,所以现在rdi就存放这字符串的真实地址,第6行movl   $0x10, %esi,%esi就是%rsi,存放的就是字符串的长度0x10,也就是16,接下来就是第8行的callq  0x100003f06,调用String.init方法,rdi和rsi就是这个方法的参数。

     第20行movabsq $0x7fffffffffffffe0, %rdx,将0x7fffffffffffffe0放到rdx中,第21行addq   %rdx, %rdi,rdi就是字符串的真实地址,rdi加上rdx的值再放到rdx中。

    所以一旦字符串的内容长度超过15,就不会将字符串的内容存储在字符串变量的16个字节里面,而是放到其他地方,然后将地址值存起来,最终我们可以根据地址值找到字符串的真实内容。

    那么字符串内容的真实地址值是在内存哪块区域呢?

    我们可以再看一下这里的汇编代码,0x1f1(%rip)就是真实地址值,一般这样的就是在全局区,所以是有可能在全局区内存里面的,先算出真实地址值0x1f1 + 0x100003d8f =  0x100003F80。

    从编码到启动APP

    OC、Swift源码 -----(编译、链接)-----> Mach-O可执行文件 ------(启动)------> 内存 (内存地址从低到高,Mach-O、动态库)

    代码区、全局区、常量区全部都在Mach-O里面。一般编译后的可执行文件载入内存后都会有一个偏移量,但是Mach-O文件的偏移量可以忽略,我们可以直接看Mach-O文件,看看字符串是在内存中的哪个区域。

    Mach-O格式的可执行文件。

    用MachOView打开Mach-O文件:

    如果00 00 00 00 00 00 00 00这8个字节在Mach-O里面是00008030的偏移量,那么他在内存中的地址就是0x100000000 +  0x8030,这个就是VM Address,虚拟内存地址。

    我们想找0x100003F80,实际上在Mach-O里面就是00003F80。

    可以在cstring里面找到,这里就是常量区,_TEXT整个可以叫代码区,__cstring就是常量区。

    如果找不到products文件夹可以通过这个方式查找:

    Xcode13 新建项目 Products 目录显示方法_蓝清水的博客-CSDN博客

    超过15长度的字符串变量前8个字节表示什么:

    1. var str1 = "01234567"
    2. var str2 = "0123456789ABCDEF"
    1. (lldb) register read rax
    2. rax = 0x3736353433323130
    3. (lldb) register read rdx
    4. rdx = 0xe800000000000000
    5. (lldb)
    1. (lldb) register read rax
    2. rax = 0xd000000000000010
    3. (lldb) register read rdx
    4. rdx = 0x8000000100003f60
    5. (lldb)

    我们可以多写一些内容看一下: 

    1. (lldb) register read rax
    2. rax = 0xd000000000000014
    3. (lldb) register read rdx
    4. rdx = 0x8000000100003f60
    5. (lldb)

     可以看出rax表示的是字符串的长度。

    思考题,拼接后内存会有什么变化呢?

    1. var str1 = "01234567"
    2. str1.append("G")
    3. var str2 = "0123456789ABCDEF"
    4. str2.append("G")

    我们可以用汇编看一下

    看str1拼接前后的变化,

    1. (lldb) x 0x100008050
    2. 0x100008050: 30 31 32 33 34 35 36 37 47 00 00 00 00 00 00 e9 01234567G.......
    3. 0x100008060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
    4. (lldb)

     append后长度不超过15就还在16个字节内存储字符串内容,超过后需要开辟新的内存,并且是动态变化的。

    我们看下str2的情况:

    开辟堆空间的话最终都会调用malloc方法,

    刚开始是放在常量区,常量区是不能进行修改的,所以append需要另外在堆空间开辟内存,堆空间地址值在str2变量的后8个字节,堆空间中的内存前32个字节是存放和对象相关的内容,之后就是字符串的真实内容了。

    总结:

    字符串初始化时长度小于等于15的,字符串内容直接存放在str变量的内存中;

    字符串初始化时长度大于15的,字符串内容存放在__TEXT, __cstring中(常量区),字符串的地址值信息存放在str变量的后8个字节中,但是需要通过计算才能得出真实的地址值;

    append后如果字符串长度小于等于15,字符串内容依然存放在str变量内存中;

     append后如果字符串长度大于15,会开辟堆空间,因为常量区是不可以进行更改的。

    dyld_stub_binder

    1、符号的延迟绑定通过dyld_stub_binder完成,callq指令只是调用的占位的地址,最终会通过符号绑定找到动态库里面的地址调用;

    2、jmpq *0xb31(%rip)格式的汇编指令

            占用6个字节

  • 相关阅读:
    【小程序源码】全新优化版趣味语句生成器
    盘点十个让工作效率倍增且有趣的 Python工具包
    C语言 3:常量和变量,顺序语句,选择语句,循环语句,作用域和生存期
    BLIP2模型加载在不同设备上
    【C++】数组中出现次数超过一半的数字
    Docker浅尝
    【校招VIP】计算机网络之TCP/IP模型归纳
    Idea运行支付宝网站支付demo踩坑解决及其测试注意事项
    elment-ui中el-table刷新
    05-SA8155 QNX Hypervisor MultiTouch多点触摸
  • 原文地址:https://blog.csdn.net/run_in_road/article/details/126433374