1.编译流程
操作
swiftc -dump-ast main.swift // 生成语法树
swiftc -emit-sil main.swift // 生成最简洁的SIL代码
swiftc -emit-ir main.swift -o main.ll // 生成LLVM IR代码
swiftc -emit-assembly main.swift -o main.s // 生成汇编代码
Contents/Developer/Toolchains/XcodeDefailt.xctoolchain/usr/bin
2.基础语法
2.1 基本运算
简单略
2.2 流程控制
简单略
2.3 函数
1、基本函数
/// 求和 【概述】
///
/// 求和 【详述】
///
/// - Parameters:
/// - v1: 参数1
/// - v2: 参数2
///
/// - Note: 传入两个Int类型的参数相加
///
func sum(v1: Int, v2: Int) -> Int {
return v1 + v2
}
var result = sum(v1: 10, v2: 20)
print(result)
2、参数标签
- 修改参数标签
func goToSleep(at time: String){
print("It's time to Sleep -> \(time)")
}
goToSleep(at: "23:00")
- 可以使用
_
省略参数标签
func sub(_ v1: Int, _ v2: Int) -> Int {
return v1 - v2
}
var res = sub(10, 20)
print(res)
3、可变参数
func sum(_ numbers: Int...) -> Int {
var res = 0
for number in numbers {
res += number
}
return res
}
print(sum(10,20,30,40,50))
// A parameter following a variadic parameter requires a label
func testFunc(_ numbers: Int..., name1:String, name2:String) {}
testFunc(10,20,30,41,name1: "MKJ", name2: "HY")
一个函数只能
最多有一个
可变参数
紧跟在可变参数后面的参数不能省略标签,报错 A parameter following a variadic parameter requires a label
4、输入输出参数
函数参数默认是let
,默认是常量
var number = 10
func test(_ num: inout Int){
num += 1
}
test(&number)
print(number)
可变参数不能标记为
inout
inout参数不能有默认值
inout参数只能传入可以被多次赋值的var
inout参数的本质是地址传递(引用传递)
5、函数重载 (Function Overload)
- 函数名相同
- 参数个数不同 或 参数类型不同 或 参数标签不同
- 返回值的类型与函数重载无关
- 默认参数和函数重载一起使用产生二义性时,编译器并不会报错(C++ 就会报错)
func sum(v1: Int, v2: Int) -> Int {
v1 + v2
}
func sum(v1: Int, v2: Int, v3: Int = 10) -> Int {
v1 + v2 + v3
}
// 有歧义,会调用第一个,但是不会报错
print(sum(v1: 10, v2: 20))
6、内联函数(函数调用直接展开成函数体)
如果开启了编译器优化,编译器会将某些函数变成内联函数(Release模式下默认开启了优化)
虽然断点了,但是控制台会直接打印输出,函数体直接内联在了main
函数里面
除非加了@inline
关键字
@inline(never) func test() {
print("Mikejing")
}
test()
以上代码就永远不会被内联,即使开了编译优化,@inline(__always)
该标识符就代表即使代码很长,也会被内联(动态派发或者递归调用除非),这里的动态派发可以理解为子类父类的多态
哪些不会被自动内联
1.函数体比较长
2.包含递归
3.包含动态派发
2.4 枚举
变量的取值就几种,就应该考虑用枚举
- 原始值
不占用枚举变量的内存
enum Season : Int {
case spring = 1, summber, autumn, winter
}
// s是枚举类型,不是Int 类型, 原始值,一一对应,存储关联
var s = Season.spring
s = .autumn
print(s)
print(s.rawValue)
print(Season.winter.rawValue)
- 关联值
和枚举存储在一起,占用内存
enum Date {
case digit(year: Int, month: Int, day: Int)
case string(String)
}
var date = Date.digit(year: 2021, month: 11, day: 2)
date = .string("Ten Grade-12-IEG")
print(date)
switch date{
case let .digit(year, month, day):
print("\(year)-\(month)-\(day)")
case let .string(name):
print("\(name)")
}
- MemoryLayout 测试枚举内存
enum Password {
case number(Int, Int, Int, Int)
case other1
case other2
}
var password = Password.number(1, 2, 3, 4)
print(MemoryLayout.size(ofValue: password)) // 实际用到的大小 33
print(MemoryLayout.stride(ofValue: password)) // 分配占用的空间大小 40
print(MemoryLayout.alignment(ofValue: password)) // 内存对齐 8
password = .other1
print(MemoryLayout.size(ofValue: password)) // 33
print(MemoryLayout.stride(ofValue: password)) // 40
print(MemoryLayout.alignment(ofValue: password)) // 8
由于关联值是和枚举存储在一起的,那么.number
有四个Int
,占用了32个字节,其他枚举都可以用一个字节来区分,因此只要33个字节,由于内存对齐是8个字节,因此,最终分配的内存是40个字节。
- 特殊案例测试
enum Grade: String {
case perfect = "A"
case great = "B"
case good = "C"
case bad = "D"
}
// print(MemoryLayout<Grade>.size) // 1
// print(MemoryLayout<Grade>.stride) // 1
// print(MemoryLayout<Grade>.alignment) // 1
enum Direction {
case north, south, east, west
}
//print(MemoryLayout<Direction>.size) // 1
//print(MemoryLayout<Direction>.stride) // 1
//print(MemoryLayout<Direction>.alignment) // 1
enum Score {
case points(Int)
case grade(Character)
case other
}
// print(MemoryLayout<Score>.size) // 17
// print(MemoryLayout<Score>.stride) // 24
// print(MemoryLayout<Score>.alignment) // 8
enum Date {
case digit(year: Int, month: Int, day: Int)
case string(String)
}
//print(MemoryLayout<Date>.size) // 25
//print(MemoryLayout<Date>.stride) // 32
//print(MemoryLayout<Date>.alignment) // 8
enum TestEnum {
case test0
case test1
case test2
case test3(Int)
case test4(Int, Int)
case test5(Int, Int, Int, Bool)
}
//print(MemoryLayout<TestEnum>.size) // 25
//print(MemoryLayout<TestEnum>.stride) // 32
//print(MemoryLayout<TestEnum>.alignment) // 8
根据上面的例子,其中原始值类型,占用都是一个字节。枚举Score
类型,Charater
占用了16个字节,再多一个字节,就能区分其他三个类型,因此只要17个字节,Date
类型也一样。再来看最后一种类型TestEnum
,个人理解是,当.test5
类型下,最少需要25个字节,那么最后一个字节,就可以用来区分作为用其他枚举类型的字节位,如果用来表示.test5
,只要有特定值即可,然后其他枚举有对应的特定值即可。
上述的内存分布可以往后查看
2.5 可选类型(optional)
- 可选项是对其他类型的一层包装,可以将它理解为一个盒子
- 如果为nil,那么它是个空盒子
- 如果不为nil,那么盒子里装的是:被包装类型的数据
- 如果要从可选项中取出被包装的数据(将盒子里装的东西取出来),需要使用感叹号! 进行强制解包
- 如果对值为nil的可选项(空盒子)进行强制解包,将会产生运行时错误
Fatal error: Unexpectedly found nil while unwrapping an Optional value
可选项绑定
可以使用可选项绑定来判断可选项是否包含值
如果包含就自动解包,把值赋给一个临时的常量(let)或者变量(var),并返回true,否则返回false
示例一:
if let number = Int("123") {
print("字符串转换整数成功:\(number)") // number是强制解包之后的Int值
// number作用域仅限于这个大括号
} else {
print("字符串转换整数失败")
}
// 字符串转换整数成功:123
示例二:条件语句中用到可选绑定,就需要,
隔开,不能用&
隔开
// 遍历数组,将遇到的正数都加起来,如果遇到负数或者非数字,停止遍历
var strs = ["10", "20", "abc", "-20", "30"]
var index = 0
var sum = 0
while let num = Int(strs[index]), num > 0 {
sum += num
index += 1
}
print(sum)
空合并运算符
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T?) rethrows -> T?
public func ?? <T>(optional: T?, defaultValue: @autoclosure () throws -> T) rethrows -> T
- a ?? b
- a 必须是可选项
- b 是可选项 或者 不是可选项
- b 跟 a 的存储类型必须相同
- 如果 a 不为nil,就返回 a
- 如果 a 为nil,就返回 b
- 如果 b 不是可选项,返回 a 时会自动解包
??
返回的类型取决于最右边的类型
let a: Int? = 1
let b: Int = 2
let c = a ?? b // c Int, 1
let a: Int? = nil
let b: Int = 2
let c = a ?? b // c Int, 2
??
和if let
配合使用
let a:Int? = nil
let b: Int? = 2
if let c = a ?? b{
print(c)
}
// 等价于 if a != nil || b != nil
字符串插值消除警告
var name: String? = "Mk"
print("\(name)")
// String interpolation produces a debug description for an optional value; did you mean to make this explicit?
print("\(name!)")
print("\(String(describing: name))")
print("\(name ?? "")")
多重可选项
var num1: Int? = nil
var num2: Int?? = num1
var num3: Int?? = nil
(num2 ?? 1) ?? 2 // 2
(num3 ?? 1) ?? 2 // 1
也可以用lldb指令
frame variable -R
或者fr v -R
查看区别
(lldb) fr v -R num1
(Swift.Optional<Swift.Int>) num1 = none {
some = {
_value = 0
}
}
(lldb) fr v -R num2
(Swift.Optional<Swift.Optional<Swift.Int>>) num2 = some {
some = none {
some = {
_value = 0
}
}
}
(lldb) fr v -R num3
(Swift.Optional<Swift.Optional<Swift.Int>>) num3 = none {
some = some {
some = {
_value = 0
}
}
}
因此,上面的(num2 ?? 1) ?? 2
第一个空合运算符,代表num2
是有值的,只是一个有值的空盒子包装,第二个运算符就是nil,取2即可。
2.6 枚举内存布局
示例一:
可以看到该枚举实际占用了25个字节,内存对齐8个字节,因此实际分配了32个字节
可以看到前 24个字节用来存储,第25个字节用来区分具体哪个case,可以看出,这里其实有四种case,分别对应第25个字节的 01,02,03,00,如果是test0,test1,test2这三个case,都是03类型,然后通过第一个字节进行值区分
enum TestEnum {
case test0
case test1
case test2
case test3(Int)
case test4(Int, Int)
case test5(Int, Int, Int)
}
print(MemoryLayout<TestEnum>.size) // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8
/*
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
02
00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, 3)
withUnsafePointer(to: &test1) { ptr in
print("address-\(ptr)")
}
print("开始")
/*
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
01
00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00
00 00 00 00 00 00 00
*/
test1 = .test3(6)
/*
02 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03
00 00 00 00 00 00 00
*/
test1 = .test2;
/*
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03
00 00 00 00 00 00 00
*/
test1 = .test1;
/*
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
03
00 00 00 00 00 00 00
*/
test1 = .test0;
示例二:
该示例如果按上面的套路,应该是24 + 1 + 1 = 26个字节,但是实际只需要25个字节,可以看到这边的Bool类型这个字节可以用来做区分不同case,因此只需要25个字节即可,编译器不至于那么傻
enum TestEnum {
case test0
case test1
case test2
case test3(Int)
case test4(Int, Int)
case test5(Int, Int, Int,Bool)
}
print(MemoryLayout<TestEnum>.size) // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8
/*
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
03 00 00 00 00 00 00 00
81
00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, 3, true)
withUnsafePointer(to: &test1) { ptr in
print("address-\(ptr)")
}
print("开始")
/*
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
40
00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00
00 00 00 00 00 00 00
*/
test1 = .test3(6)
/*
02 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
C0
00 00 00 00 00 00 00
*/
test1 = .test2;
/*
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
C0
00 00 00 00 00 00 00
*/
test1 = .test1;
/*
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
C0
00 00 00 00 00 00 00
*/
test1 = .test0;
示例三:
该示例Bool类型不是最后一位,因此内存对齐的原因,实际都需要32位来支撑,但是不需要额外的一个字节来区分不同case,Bool类型占用的那8个字节,低地址最后一个就可以用来区分不同case,高地址01就可以用来表示Bool值了
enum TestEnum {
case test0
case test1
case test2
case test3(Int)
case test4(Int, Int)
case test5(Int, Int, Bool, Int)
}
print(MemoryLayout<TestEnum>.size) // 32
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8
/*
01 00 00 00 00 00 00 00
02 00 00 00 00 00 00 00
01 00 00 00 00 00 00 80
03 00 00 00 00 00 00 00
*/
var test1 = TestEnum.test5(1, 2, true, 3)
withUnsafePointer(to: &test1) { ptr in
print("address-\(ptr)")
}
print("开始")
/*
04 00 00 00 00 00 00 00
05 00 00 00 00 00 00 00
00 00 00 00 00 00 00 40
00 00 00 00 00 00 00 00
*/
test1 = .test4(4, 5)
/*
06 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
*/
test1 = .test3(6)
/*
02 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 C0
00 00 00 00 00 00 00 00
*/
test1 = .test2;
/*
01 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 C0
00 00 00 00 00 00 00 00
*/
test1 = .test1;
/*
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 C0
00 00 00 00 00 00 00 00
*/
test1 = .test0;
总结:
1.一个字节存储成员值(多个case的情况,如果有Bool的情况下,可以共用这8个字节或者这1个字节)
2.N个字节存储关联值(N取占用内存最大的关联值),任何一个case的关联值/原始值都共用这N个字节
3.AT&T汇编和lldb指令
3.1寄存器和内存
通常,CPU会先将内存中的数据存储到寄存器中,然后再对寄存器中的数据进行运算
假设内存中有块红色内存空间的值是3,现在想把它的值加1,并将结果存储到蓝色内存空间
- 1.CPU首先会将红色内存空间的值放到rax寄存器中:
movq 红色内存空间, %rax
- 2.然后让rax寄存器与1相加:
addq
movq %rax, 蓝色内存空间
x1, %rax - 3.最后/z将值赋值给内存空间:
- x86和x64汇编根据编译器的不同,有两种书写格式
- Intel:Window派系
- AT&T汇编 — iOS模拟器
- AT&T:Unix派系
咱们iOS开发,最主要的汇编语言是: - ARM汇编 — iOS真机
以下是常见汇编指令
项目 | AT&T | Inter | 说明 |
寄存器命名 | %rax | rax | |
操作数顺序 | movq %rax, %rdx | mov rdx, rax | 将rax的值赋值给rdx |
常数、立即数 | movq $3, %rax movq $0x10 %rax | mov rax, 3 mov rax, 0x10 | 将3赋值给rax,将0x10赋值给rax |
内存赋值 | movq $0xa, 0x1ff7(%rip) | mov qword ptr [rip+0x1ff7], 0xa | 将0xa赋值给地址为rip+0x1ff7的内存空间 |
取内存地址 | leaq -0x18(%rbp), %rax | lea rax, [rbp-0x18] | 将rbp-0x18这个地址赋值给rax |
jmp指令 | jmp *%rdx jmp 0x4001002 jmp *(%rax) | jmp rdx jmp 0x4001002 jmp [rax] | call 和 jmp写法类似 |
操作数长度 | movl %eax, %edx movb $0x10 %al leaw 0x10(%dx), %ax | mov edx eax mov al 0x10 lea ax, [dx + 0x10] | b=byte (8-bit) s = short(16-bit integer or 32-bit floating point) w = word (16-bit) l = long (32-bit integer or 64-bit floating point) q = quad (64 bit) t = ten bytes (80 -bit floating point) |
16个常用寄存器
- r8,r9,r10,r11,r12,r13,r14,r15
- rax、rdx常作为函数返回值使用
寄存器具体用途
- rdi、rsi、rdx、rcx、r8、r9等常用于存放函数参数
- rsp和rbp用于栈操作
- rip作为指令指针(存储着CPU下一条要执行的指令的地址,一旦CPU读取一个指令,rip会自动指向下一个指令)
3.2 lldb常用指令
enum TestEnum {
case test0
case test1
case test2
case test3(Int)
case test4(Int, Int)
case test5(Int, Int, Int)
}
print(MemoryLayout<TestEnum>.size) // 25
print(MemoryLayout<TestEnum>.stride) // 32
print(MemoryLayout<TestEnum>.alignment) // 8
var test1 = TestEnum.test5(10, 20, 30) // 断点到此处,开启汇编Debug模式
- 读取寄存器的值
register read/格式 、 register read/x - 修改寄存器的值
register write 寄存器名称 数值 、 register write rax 0 - 读取内存中的值
x/数量-格式-字节大小 内存地址
x/3xw 0x00000100 - 格式
x是16进制 f是浮点 d是十进制 - 字节大小
b - byte 1字节
h -half word 2字节
w - word 4字节
g - giant word 8字节 - 修改内存中的值
memory write 内存地址 数值
memory write 0x000001010 - thread step-over、next、n
单步运⾏行行,把子函数当做整体⼀一步执⾏行行(源码级别) - thread step-in、step、s
单步运⾏行行,遇到子函数会进⼊入子函数(源码级别) - thread step-inst-over、nexti、ni
单步运⾏行行,把子函数当做整体⼀一步执⾏行行(汇编级别) - thread step-inst、stepi、si
单步运⾏行行,遇到子函数会进⼊入子函数(汇编级别) - thread step-out、finish
直接执⾏行行完当前函数的所有代码,返回到上一个函数(遇到断点会卡住)
看完上述基本的汇编指令和lldb指令,我们从汇编的角度看下枚举的汇编代码
0x100001b5d <+2845>: movq rip
xa, 0x3b48(%rip) ; Swift01.date : Swift01.Date + 28
0x100001b68 <+2856>: leaq 0x3b41(%rip), %rax ; Swift01.test1 : Swift01.TestEnum
0x100001b6f <+2863>: movq CPU
x14, 0x3b3e(%rip) ; Swift01.test1 : Swift01.TestEnum + 4
0x100001b7a <+2874>: movq si
x1e, 0x3b3b(%rip) ; Swift01.test1 : Swift01.TestEnum + 12
0x100001b85 <+2885>: movb 0x100001b5d <+2845>: movq xa, 0x3b48(%rip)
可以看到 rip的地址就是下一条的地址 0x100001b68
0x3b48 + 0x100001b68 = 0x1000056B0 把0xa 存入 0x1000056B0 q占用八个字节 以下依次类推
0x100001b68 <+2856>: leaq 0x3b41(%rip), %rax 0x100001b6f + 0x3b41 = 0x1000056B0 地址放入 rax
0x100001b6f <+2863>: movq x14, 0x3b3e(%rip) 0x1000056B8 存 20
0x100001b7a <+2874>: movq x1e, 0x3b3b(%rip) 0x1000056C0 存 30
0x100001b85 <+2885>: movb x2, 0x3b3c(%rip) 0x1000056C8
存 2 类型关联值 b 一个字节
x2, 0x3b3c(%rip) ; Swift01.test1 : Swift01.TestEnum + 23
可以看到如下汇编
记住如下两条原则:
- 存储的是指令的地址
- 要执行的下一条指令地址就存储在rip中
- 汇编单步调试,进入函数
分析如下
看到对应的地址下存储的值
规律知识点
1.内存地址格式为 0x4bdc(%rip),一般是全局变量,全局区(数据段)
2.内存地址格式为 -0x78(%rbp),一般是局部变量,栈空间
3.内存地址格式为 0x10(%rax),一般是堆空间
4. 小括号里面放的都是内存地址
5. r开头: 64bit 8个字节
e开头: 32bit 4个字节
ax,bx,cx:16bit, 2个字节
ah,al:1个字节
bh,bl
movq 0xa %rax
0xa 0x0 0x0 0x0 0x0 0x0 0x0 0x0 0x0
第一天结束,明天继续!!!!!!