汇编分析Swift中的String

 

关于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个字节

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Win_77

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值