您现在的位置是:首页 >技术交流 >超详细:实现 Swift 与 汇编(Asm)代码混编并在真机或模拟器上运行网站首页技术交流

超详细:实现 Swift 与 汇编(Asm)代码混编并在真机或模拟器上运行

大熊猫侯佩 2024-10-07 00:01:03
简介超详细:实现 Swift 与 汇编(Asm)代码混编并在真机或模拟器上运行

在这里插入图片描述

功能需求

虽然现在  开发的绝对主角是 Swift 语言,不过我们也希望有时 Swift 能够调用小段汇编代码以完成特殊功能。

在本篇博文中,您将学到如下内容:

  • Swift 与 汇编语言混编的基本原理;
  • 如何在模拟器中使用 Swift + x64 汇编指令?
  • 如何在真机中使用 Swift + ARM64 汇编指令?
  • Xcode 如何根据不同编译目标切换不同类型的汇编指令?
  • 实例测试真机上汇编语言执行速度;

关于更多 Swift、C、 intel x64 和 ARM64 汇编语言执行效率比较,请移步如下链接观赏:


请系好安全带,本次航行我们将穿越量子微宇宙。

Let‘s go!!!?

功能分析

1. Swift 与 汇编如何才能成为“好基友”?

大家都知道,在 C 或 C++ 等其它高级语言中我们都可以通过内嵌汇编语言(Inline assembly)的方式来与它方便的“打成一片”。

比如,在以下 C 代码中我们就以内嵌形式调用了汇编代码:

#include <stdio.h>
 
extern int func(void);
// the definition of func is written in assembly language
__asm__(".globl func
	"
        ".type func, @function
	"
        "func:
	"
        ".cfi_startproc
	"
        "movl $7, %eax
	"
        "ret
	"
        ".cfi_endproc");
 
int main(void)
{
    int n = func();
    // gcc's extended inline assembly
    __asm__ ("leal (%0,%0,4),%0"
           : "=r" (n)
           : "0" (n));
    printf("7*5 = %d
", n);
    fflush(stdout); // flush is intentional
 
    // standard inline assembly in C++
    __asm__ ("movq $60, %rax
	" // the exit syscall number on Linux
             "movq $2,  %rdi
	" // this program returns 2
             "syscall");
}

不过,可能是  考虑到安全和多平台移植等诸多烦心的事, Swift 语言本身是没有内嵌汇编语言机制的。

所以,为了 Swift 能与汇编 成为“好基友”,我们需要找到 Swift 代码识别并调用汇编的方法:这可以通过桥接 Objective-C(后面简称为:Objc) 代码来实现

2. Objc:缘分一道桥

首先,用 Xcode 创建名为 ‘Asm_X64’ (iOS 类型)的项目,选择 Swift 作为项目主语言。

在 Swift 项目中桥接 Objc 代码很容易,我们只需在 Xcode 中新建一个 Objc 源代码文件,随后 Xcode 会自动为我们创建桥接头文件。

为了简单起见,我们可以直接选择创建 Cocoa Touch Class 类型,这会同时生成 .h 头文件和 .m 文件:

在这里插入图片描述


理论上,我们可以不需要 .m 文件,只需要一个 .h 文件即可,因为本例中 Swift 需要的所有代码都是由汇编而不是 Objc 来实现的。

不过,在项目中只创建一个 .h 文件无法让 Xcode 自动生成桥接头文件(Bridging Header)。

所以,如果大家不嫌手动创建桥接头文件麻烦的话,可以只创建 .h 文件。


我们打算用 Objc 类作为跳板来让 Swift 间接调用汇编代码。So,为我们的 Objc 类起名为 AsmHelper:

在这里插入图片描述


提示:我们后面会介绍 Swift 如何不通过 Objc 类作为跳板来调用汇编代码的方法。


紧接着,Xcode 会自动提示是否创建 Objc 的桥接头文件(Objective-C bridging header),这里当然是选择 创建 了:

在这里插入图片描述

在新创建的 AsmHelper 类中添加一个 calc_max 方法,现在该方法啥也不做只是默默的返回 0:

// AsmHelper.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

@interface AsmHelper : NSObject
-(int)calc_max;
@end

NS_ASSUME_NONNULL_END

// ****************************************************

// AsmHelper.m
#import "AsmHelper.h"

@implementation AsmHelper
- (int)calc_max {
	return 0;
}
@end

还差一步:打开 AsmTest-Bridging-Header.h 桥接头文件,导入 AsmHelper.h。

// AsmTest-Bridging-Header.h 
#import "AsmHelper.h"

准备工作已就绪,下面终于可以进入“正题”了!

3. 如何在模拟器中使用 Swift + x64 汇编指令?

根据我们 Mac 机器中不同的处理器,我们需要分别为 intel 和  Silicon (M1,M2)两种芯片来考虑开发设置。

3.1 intel 芯片的 Mac

如果你使用 intel 芯片的 Mac 来进行开发,那么模拟器实际是在 intel 处理器上运行的。所以我们需要使用 intel x64 类型的汇编指令。

在 Xcode 项目中新建一个 max_x64.s 文件用来存放汇编代码。为了方便 Objc 调用,我们在 max_x64.s 中写了一个函数:

	.text
    .globl      _calc_max_x64_asm
    .p2align    4, 0x90
   	// 方法进入点
_calc_max_x64_asm:
	// 方法构造前缀
    push    %rbp
    mov     %rsp,%rbp
    pushq   %rbx
    pushq   %rdx

	/* 
		实际代码将在此...
	*/

	// 方法析构后缀
	popq    %rdx
    popq    %rbx
    popq    %rbp
    // 返回值放在 rax 寄存器中
    mov     $11,%rax
    ret

需要说明一下:上面 .globl 伪指令的作用是将 _calc_max_x64_asm 作为全局标签来对待,这样我们就可以从外部(Objc中)找到它。

其实从汇编指令层面上来说,是没有所谓“函数或方法”概念的。只要满足以下 5 点要求,任何指令块都可以作为函数或方法来调用:

  1. 确定指令执行的起始地址(方法进入点);
  2. 设置栈;
  3. 处理寄存器的保存和恢复;
  4. 处理传入参数并设置返回值;
  5. 添加返回指令(ret);

以上这些都属于应用程序二进制接口编程(ABI)的范畴,不同语言和系统的实现都有所不同,我们需要按照对应的规范来编写代码。

对于上面的“函数”来说:它没有传入参数,并且有一个 int 类型的返回值。


关于更多汇编语言的知识,感谢兴趣的小伙伴们可以移步到我汇编(Asm)专栏中观赏相关文章:


回到 AsmHelp.h 文件中,我们需要在 Objc 中声明汇编函数的签名:

// AsmHelp.h
extern int calc_max_x64_asm(void);

接着,在 AsmHelp 类的 calc_max 实例方法中调用汇编实现的 calc_max_x64_asm 函数:

@implementation AsmHelper
- (int)calc_max {
    return calc_max_x64_asm();
@end

现在,我们可以回到 Swift 语言中通过 Objc 调用 calc_max_x64_asm 函数了:

import SwiftUI

struct ContentView: View {
    @State var max = 0
    
    var body: some View {
        Text("max is (max)")
            .font(.title)
            .onAppear {
                max = AsmHelper().calc_max()
            }
    }
}

在模拟器或 Xcode 预览(Preview)中运行上述代码,结果不出所料:

在这里插入图片描述

3.2 Apple Silicon 芯片的 Mac

 Silicon 处理器(M1,M2等)本身就兼容 arm 指令集,所以对于此 Mac 中模拟器汇编语言的适配,可以按照下面 “在 iPhone 真机中” 运行的方案来对待。


小知识:其实在  Silicon 上也可以用 intel x64 “仿真”模式运行模拟器,只需在 Xcode 中设置 Destination Architectures 为 ‘Show Rosetta Destinations’ (或 Show Both)即可:

在这里插入图片描述


4. 如何在真机中使用 Swift + ARM64 汇编指令?

如果在 Xcode 中将构建目标由模拟器变为真机,那么编译上面包含 intel x64 汇编代码的项目 ‘Asm_X64’ 会妥妥的报错:

在这里插入图片描述

这是因为  真机设备的处理器是 arm 架构,要想在真机上运行我们需要使用 ARM64 种类的汇编代码。


  • iPhone 14PM(Pro Max)使用 A16 处理器(4纳米)
  • iPhone XR 使用 A12处理器(7纳米)

在文章最后,我们会分别在以上两种真机上运行 ARM64 汇编测试代码,并比较它们的耗时情况。


现在,为了不与之前 intel x64 汇编代码混淆,我们新建另一个名为 ‘Asm_ARM64’ 的 Xcode 项目,其中 AsmHelper 类的实现和桥接头文件的内容和之前完全相同。

接着,我们创建 max_arm64.s 文件,并填入如下代码:

    .text
    .globl  _calc_max_arm64_asm
    .p2align    2
    // 方法进入点
_calc_max_arm64_asm:
    // 方法构造前缀
    sub     sp,sp,#32
    stp     x29,x30,[sp,#16]
    add     x29,sp,#16

    /* 
        实际代码将在此...
    */
    
    // 方法析构后缀
    ldp     x29,x30,[sp,#16]
    add     sp,sp,#32
    // 返回值放在 x0 寄存器中
    mov     x0,$11
    ret

可以看到:ARM64 实现的函数和 intel x64 汇编类似,都符合各自 ABI 接口的规范。

该项目的 Swift 代码也和 ‘Asm_X86’ 项目中的如出一辙。我们选择真机作为构建目标,编译运行后的结果也应该和之前模拟器上的完全相同。

5. Xcode 如何根据不同编译目标切换不同类型的汇编代码?

细心的小伙伴们可能察觉到了:把 intel x64 和 ARM64 汇编代码分散在两个不同项目中既 不利于测试 又 违反了 DRY 原则。

能不能把它们放在同一个项目中,而根据不同构建目标(真机或模拟器)来切换对应的汇编代码呢?

必须可以!!!

5.1 ”新目标,新征程“

我们回到上面 ‘Asm_X64’ 项目里,在原目标(TARGET)基础之上“克隆”一个新目标:

在这里插入图片描述

修改新目标的名称为 AsmTestARM:

在这里插入图片描述

接着,打开 Scheme 管理界面,同样将新 Scheme 名称也改为 AsmTestARM:

在这里插入图片描述

现在我们有了两个 Scheme:AsmTest 和 AsmTestARM,分别对应于 x86 和 ARM64 汇编代码文件。

关键的部分来了,我们导入之前 ‘Asm_ARM64’ 项目中的 ARM64 汇编文件 max_arm64.s,并将它的 Target 成员关系设为 AsmTestARM:

在这里插入图片描述

同样的,将原来的 x64 汇编文件 max_x64.s 的 Target 成员关系设为 AsmTest:
在这里插入图片描述

5.2 条件编译走起!

上面,我们已经把不同汇编代码文件划分到了对应的 Scheme 中,接下来还需要根据不同构建目标,来选择实际使用的汇编函数。

按如下代码修改 AsmHelper 类中的 calc_max 实例方法:

@implementation AsmHelper
- (int)calc_max {
#if TARGET_IPHONE_SIMULATOR
    return calc_max_x64_asm();
#elif TARGET_OS_IPHONE
    return calc_max_arm64_asm();
#endif
}
@end

如上代码所示,我们使用 #if 宏指令来选择调用合适的汇编函数。同样的,在 AsmHelper.h 中也按相同条件来声明对应的外部函数:

#if TARGET_IPHONE_SIMULATOR
extern int calc_max_x64_asm(void);
#elif TARGET_OS_IPHONE
extern int calc_max_arm64_asm(void);
#endif

原 Swift 代码无需做任何修改。

现在,我们可以根据不同构建目标(模拟器或真机)来切换 Scheme 了:

在这里插入图片描述

至此,项目中不同类型的汇编代码在模拟器或真机上编译运行都不会有任何问题了,棒棒哒!?

6. 跳过 Objc 直接与汇编代码混编

经过不懈努力,我们完成了 Swift 与汇编语言的混合编译。

不过,实现中使用了 Objc 作为“媒介”,仔细考虑会发现这纯属多余:因为 AsmHelper 类基本自己啥也没干!

Swift 能不能完全甩开 Objc 直接与汇编“在一起”呢?

答案是肯定的!

删除项目中的 AsmHelper.m 文件,并只保留 AsmHelper.h 文件中相关的条件编译代码:

// AsmHelper.h
#import <Foundation/Foundation.h>

NS_ASSUME_NONNULL_BEGIN

#if TARGET_IPHONE_SIMULATOR
extern int calc_max_x64_asm(void);
#elif TARGET_OS_IPHONE
extern int calc_max_arm64_asm(void);
#endif

/*
@interface AsmHelper : NSObject
-(int)calc_max;
@end
*/

NS_ASSUME_NONNULL_END

回到 Swift 的 ContentView 结构中,创建 calc_max() 方法,其中我们根据目标的架构来直接选择实际的汇编函数。

完整代码如下:

import SwiftUI

struct ContentView: View {
    @State var max = 0
    
    private func calc_max() -> Int {
#if arch(x86_64)
        return Int(calc_max_x64_asm())
#elseif arch(arm64)
        return Int(calc_max_arm64_asm())
#else
        return -1
#endif
    }
     
    var body: some View {
        Text("max is (max)")
            .font(.title)
            .onAppear {
                max = calc_max()
            }
    }
}

大功告成,Objc 再见!

7. 实例测试真机上汇编语言执行效率;

搞定“超超超难”剑桥面试数学题番外篇:ARM64汇编 这篇博文中,我们在 M2 处理器上测试了 ARM64 汇编的表现。

现在,我们用相同的汇编代码在 iPhone 真机上测试一下。

将上面 max_arm64.s 文件的代码改为如下内容:

.equ        total, 63
    .text
    .globl  _calc_max_arm64_asm
    .p2align    2
_calc_max_arm64_asm:
    sub     sp,sp,#32
    stp     x29,x30,[sp,#16]
    add     x29,sp,#16

    mov     w0,1    // a in w0
    mov     w1,w0   // b in w1
    mov     w2,w0   // c in w2
    mov     w3,w0   // d in w3
    mov     w11,wzr // max in w11
start_a_loop:
    cmp     w0,total
    b.hi    end_a_loop
start_b_loop:
    cmp     w1,total
    b.hi    end_b_loop
start_c_loop:
    cmp     w2,total
    b.hi    end_c_loop
start_d_loop:
    cmp     w3,total
    b.hi    end_d_loop
    // 计算 a + b + c + d 的值
    add     w4,w0,w1
    add     w4,w4,w2
    add     w4,w4,w3
    cmp     w4,total
    b.ne    not_equ_63
    // 若等于 a + b + c + d = 63,则计算 ab + bc + cd 的值 x
    mul     w4,w0,w1
    mul     w5,w1,w2
    mul     w6,w2,w3
    add     w5,w5,w6
    add     w4,w4,w5
    // 若 x > max ,则需要更新 max 为 x 值
    cmp     w4,w11
    b.ls    not_equ_63
    mov     w11,w4
not_equ_63:
    add     w3,w3,#1
    b       start_d_loop
end_d_loop:
    mov     w3,wzr
    add     w2,w2,#1
    b 		start_c_loop
end_c_loop:
    mov     w2,wzr
    add     w1,w1,#1
    b       start_b_loop
end_b_loop:
    mov     w1,wzr
    add     w0,w0,#1
    b       start_a_loop
end_a_loop:
    ldp     x29,x30,[sp,#16]
    add     sp,sp,#32
    // 将计算出来的最大值通过 x0 寄存器返回
    mov     x0,x11
    ret
string: .asciz  "max is %ld
"

将 ContentView 中的 body 也略作更改:

var body: some View {
    Text("max is (max)")
        .font(.title)
        .onAppear {
            let start = Date.now
            let result = calc_max()
            print("(String(format: "耗时 %0.5f", start.distance(to: Date.now)))")
            max = result
        }
}

在 iPhone XR 和 iPhone 14 Pro Max 真机运行上面的汇编代码,差距并没有想象那么大:

// 汇编代码执行耗时

// iPhone XR
耗时 0.01920 秒

// iPhone 14PM
耗时 0.01469

为了便于大家横向比较,同样贴出纯 Swift 优化后(Release)代码的执行结果:

// Swift 代码执行耗时

// iPhone XR
耗时 0.02132 秒

// iPhone 14PM
耗时 0.01704

可以发现,在 iPhone 上我们的汇编执行速度要比优化后的 Swift 代码还要快。

此役,在  A系列芯片上汇编终于扳回一局!!! ❤️


同样将 Swift 语言的测试代码贴在下方,以满足小伙伴们的好奇心:

typealias GroupNumbers = (a: Int, b: Int, c: Int, d: Int, rlt: Int)

@inline(__always) fileprivate func value(_ g: GroupNumbers) -> Int {
    g.a * g.b + g.b * g.c + g.c * g.d
}

func calc_max_swift() -> Int {
    var max = 0
    let r = 1...63
    for a in r {
        for b in r {
            for c in r {
                for d in r {
                    if a + b + c + d == 63 {
                        let v = (a: a, b: b, c: c, d: d, rlt: 0)
                        let rlt = value(v)
                        
                        if rlt >= max {
                            max = rlt
                        }
                    }
                }
            }
        }
    }

    return max
}

总结

在本篇博文中,我们图文并茂的详细介绍了如何在真机或模拟器上运行 Swift 和汇编混编后的代码,并通过一个实际例子来测试不同 iPhone 上汇编的执行效率。

最后,还得问一下小伙伴:你们学会了吗??

结束语

Hi,我是大熊猫侯佩,一名非自由App开发者,希望我的文章可以解决你的痛点、难点问题。

如果还有问题欢迎在下面一起讨论吧 ?

感谢观赏,再会。

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。