关于String的思考
1、1个String变量占用多少内存?
2、下面2个String变量,底层存储有什么不同?
var str1 = "0123456789"
var str2 = "0123456789ABCDEF"
意思就是这两个字符串在内存中分别是存储在哪个区域的。
内存 地址 从低 到高 | 代码区 |
常量区 | |
全局区(数据段) | |
堆空间 | |
栈空间 | |
动态库 |
3、如果对String进行拼接操作,String变量的存储会发生什么变化?
str1.append("ABCDE")
str1.append("F")
str2.append("G")
4、ASCII码表:https://www.ascii-code.com/
汇编分析String
str1最少是有16个字节的,从8、9行就可以看出,通过MemoryLayout.stride()打印也可以看出来。
在第10行处打断点,进行打印可以获取str1变量16个字节存储的东西:
(lldb) register read rax
rax = 0x3736353433323130
(lldb) register read rdx
rdx = 0xea00000000003938
(lldb)
rip的地址加上0x409f,也就是0x100003f71 + 0x409f = 0x100008010,这个就是str1的内存地址,通过以下命令可以获取该地址存储的内容:
(lldb) x/2xg 0x100008010
0x100008010: 0x3736353433323130 0xea00000000003938
(lldb)
发现和上面打印出来的内容是相同的,所以我们从各个方面证明了str1变量存储的内容就是 0x3736353433323130 0xea00000000003938,占16个字节。
以上是ASCII码值表,0对应16进制是0x30,1对应0x31,对应着看str1存储的内容,可以发现是从30一直到39的,相当于字符串的内容就直接放到了str1的内存当中了。
我们换种打印方式可以看的更加清晰:
(lldb) x 0x100008010
0x100008010: 30 31 32 33 34 35 36 37 38 39 00 00 00 00 00 ea 0123456789......
0x100008020: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(lldb)
0xea又代表什么内容呢?
我们把9删掉:
var str1 = "012345678"
根据同样的方式获取到str1存储的内容:
(lldb) register read rax
rax = 0x3736353433323130
(lldb) register read rdx
rdx = 0xe900000000000038
(lldb)
可以发现0xea变成了0xe9,所以a\9这一位是用来存储字符串长度的,0xe表示的是字符串的存储方式,a\9这一位最大是f,f的时候就是刚刚好填满15个字节,也就是最多存储15个字符。
我们可以来测试一下:
var str1 = "0123456789ABCDE"
(lldb) register read rax
rax = 0x3736353433323130
(lldb) register read rdx
rdx = 0xef45444342413938
(lldb)
再看一下ASCII码表:
发现确实是和我们的结论一致的。
小于等于15位的时候,是直接存储在字符串变量的内存中的,类似于OC的tagger pointer。
如果再多一位会发生什么事情呢?
var str2 = "0123456789ABCDEF"
(lldb) register read rax
rax = 0xd000000000000010
(lldb) register read rdx
rdx = 0x8000000100003f70
(lldb)
我们可以再次尝试,从而找到规律:
var str2 = "0123456789ABCDEFFDSFSDFDSF"
(lldb) register read rax
rax = 0xd00000000000001a
(lldb) register read rdx
rdx = 0x8000000100003f70
(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个字节存储的是:
(lldb) register read rdx
rdx = 0x8000000100003f60
(lldb) register read rax
rax = 0xd000000000000010
(lldb)
字符串的真正内容是和0x8000000100003f60这8个字节相关的,通过汇编代码我们可以知道,
字符串的真实地址 + 0x7fffffffffffffe0 = 0x8000000100003f60,那么可以推导出
字符串的真实地址 = 0x8000000100003f60 - 0x7fffffffffffffe0。
经过计算0x100003F80就是字符串的真实地址。我们打印一下这个地址存储的内容:
(lldb) x 0x100003F80
0x100003f80: 30 31 32 33 34 35 36 37 38 39 41 42 43 44 45 46 0123456789ABCDEF
0x100003f90: 00 31 00 0a 00 20 00 00 e8 fd ff ff 03 00 00 00 .1... ..........
(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个字节表示什么:
var str1 = "01234567"
var str2 = "0123456789ABCDEF"
(lldb) register read rax
rax = 0x3736353433323130
(lldb) register read rdx
rdx = 0xe800000000000000
(lldb)
(lldb) register read rax
rax = 0xd000000000000010
(lldb) register read rdx
rdx = 0x8000000100003f60
(lldb)
我们可以多写一些内容看一下:
(lldb) register read rax
rax = 0xd000000000000014
(lldb) register read rdx
rdx = 0x8000000100003f60
(lldb)
可以看出rax表示的是字符串的长度。
思考题,拼接后内存会有什么变化呢?
var str1 = "01234567"
str1.append("G")
var str2 = "0123456789ABCDEF"
str2.append("G")
我们可以用汇编看一下
看str1拼接前后的变化,
(lldb) x 0x100008050
0x100008050: 30 31 32 33 34 35 36 37 47 00 00 00 00 00 00 e9 01234567G.......
0x100008060: 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 ................
(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个字节