macOS 汇编指南
现在很多汇编的学习资料、途径和工具都是关于 Windows 下的,所以这里来介绍一下 macOS 上学习使用汇编需要的资料和工具。为什么需要学习汇编(使用途径)汇编是计算机的“魔法”,虽然做个只会高级语言的“战士”也可以,但是当给“武器”附魔之后,战斗力也会大大增加(当然也有“玩火自焚”的)。在现代,学习汇编之后的使用途径有几种:直接用汇编指令写程序的代码,然后使用汇编器(Assembler)汇编
现在很多汇编的学习资料、途径和工具都是关于 Windows 下的,所以这里来介绍一下 macOS 上学习使用汇编需要的资料和工具。
本文持续更新中,也是作为个人笔记来使用的。
为什么需要学习汇编(使用途径)
汇编是计算机的“魔法”,虽然做个只会高级语言的“战士”也可以,但是当给“武器”附魔之后,战斗力也会大大增加(当然也有“玩火自焚”的)。
在现代,学习汇编之后的使用途径有几种:
- 直接用汇编指令写程序的代码,然后使用汇编器(Assembler)汇编成程序(这种学习的过程中可能使用比较多,在实际情况下很少用,因为太复杂了)。
- 用在 C 语言代码中,提高性能和速度,或者实现一些特别的功能。例如 UNIX 和 Linux 中的汇编代码就是为了提高运行速度,不然完全可以用纯 C 语言写出来;还有苹果不让开发者 iOS 平台获取 CPU 频率之后,一些开发者通过在 Objective-C 代码中使用汇编指令来推测频率。
注1:不过苹果后来加了转换,让推测的结果很不准,让这种 app 彻底完犊子了。
注2:Objective-C 是 C 语言的一个超集,就像 C++ 也是 C 语言的一个超集,它们都属于 C 语言家族。在苹果推出 Swift 之前,苹果平台的开发全靠 Objective-C。
- 能看懂反编译出来的内容(这种一般常规程序员用不到)。
不过在现在,汇编并不是编程的门槛,而是通往高手的必修课。但是新手还是建议先学会 C 语言再接触汇编(这里的学会是指能看明白各种结构即可)。
相关资料推荐
在现在各种高级语言、脚本语言遍地开花的今天,汇编语言越来越不受欢迎,因为繁琐、复杂,学习成本极高。例如, Intel CPU 开发手册就有 5000 页(每次发新的 CPU 之后都会更新,或增或减)。
第一个推荐的是苹果官方的汇编指南:《Mac OS X Assembler Guide》,下载地址在这里。
这本指南包含了 Mac OS X 汇编器as
的使用,汇编指令和地址模式的介绍等内容。
需要注意的是,它不光包括了 Intel CPU 的汇编内容,还包括更早期的 Power PC 的汇编内容。
但是由于该指南最新版本是 2005 年的,Intel 的指令有了巨大的更新,例如 AVX 等。并且苹果也已经在转向自己的 M1 芯片了(有意思的是,OS X 的汇编器手册最新更新时间是 2020 年6月23日,而这就是 M1 更新的日子,不知道苹果会不会在稳定之后再次更新)。
第二个推荐的是 Intel 的 《Introduction to x64 Assembly》,这是一篇汇编快速入门。新手可以看看,干货浓度极高。
第三个是:Intel 的开发手册。
这个手册虽然很长,但是一个很不错的资料,而且更新频率很高,如果需要了解新的 Intel 指令,那么这就是必须要看的了。所以这篇属于经常需要翻阅的。当然如果想见识见识也可以看看。链接是:Intel 64 和 IA-32 处理器相关的文档
可以直接下载合订本当作存档,不过这样不方便阅读。(关于这个,曾经 Intel 可以免费提供纸质版,只要你发邮件提供地址即可,有不少人定,不过大多都垫了显示器或者吃灰了。现在 Intel 不提供这个福利了,挺可惜的)
第一卷是一些大致的介绍,以及 Intel CPU 发展历程和区别,如果是初学者可以瞅瞅;第二卷开始就是指令的介绍了。第2卷的日常使用率比较高,需要经常查阅。具体区别在之前的链接中的界面可以看到。
这里需要注意的是,虽然常说 Intel 挤牙膏,但是 Intel 几乎每一代都会有指令、寄存器的更新和更改。所以文档的更新频率比较高,如果你需要使用最新的指令,那么请及时更新自己存储的文档。
第四个是一组关于 ARM 汇编的文档。
首先因为 Mac 现在以及转向自研的 M1 芯片了,M1 芯片是 ARM 架构的。
其次因为 ARM 现在并不像 Intel 一样提供了汇编指南《ARM ® DeveloperSuite Assembler Guide
Version 1.2》(这篇指南最新的b版本是2001年的),而是分散开了。如果想入门,阅读这本2001年的指南即可。
如果想深入了解最新的指令,那么查看这里https://developer.arm.com/architectures/instruction-sets。
汇编器以及实用工具介绍
汇编器(Assembler)和编译器(Complier)是不同的。如果搞不明白的话,可以简单理解成汇编器是将汇编指令转换成程序的;编译器是将高级语言的代码(例如 C/C++、Java 等的代码)转换成程序的。
这里介绍一下汇编器以及可能用得到的实用工具们(包括编辑器)。
as:
Mac OS X 的汇编器,需要在“终端”中使用,类似于 Windows 的 masm。支持 Intel 处理器的汇编以及 Power PC 处理器的汇编。放在/usr/bin/as
目录下。在《Mac OS X Assembler Guide》中有使用方法的介绍。
ld:
连接器。
clang:
clang 有个状态就是汇编器,而且可以 C 语言等代码预处理成汇编语言代码,详细操作请看这里《clang 如何产生汇编代码文件》。使用 clang 可能对你更方便,不过需要自己判断了。
gcc:
gcc 和 clang具体区别这里不赘述,gcc 有个选项:-nostartfiles
,使用这个命令可以直接忽略被链接的标准库文件和初始化行为。这是因为有时候会出现不需要连接 C 标准库,直接汇编即可,那么就需要这样。最后会有相关演示。
size:
输出对象文件的各部分大小,如下:
otool:
查看某部分大小和内容,类似 Windows 中的 debug.exe。例如查看__TEXT,__text
部分的内容:
我们也可以使用otool
来查看其他部分的内容,用以下命令格式:
$ otool -v -s __TEXT __cstring a.out
a.out:
Contents of (__TEXT,__cstring) section
0000000000000025 hello world!\n
clang 或 gcc:
Mac OS X 上,默认的 C、C++、Objective-C 编译器。如果在“终端”里使用命令cc
,那么会调用 clang,而不是 gcc。
Xcode:
苹果自己的 IDE,有图形界面,也有一些终端命令行工具,可以编写 C、C++、Objective-C 和 Swift 语言的程序。可以利用 Xcode 来编写含有汇编代码的程序和 App。
语法区别
很多人可能对汇编有所了解,但可能都是基于 Windows 的。但是在 Mac OS X 或者 C 语言(这里 C 语言编译器是 clang)内嵌汇编语言的语法中是不一样的。前者是 Intel 语法,后者是 AT&T 语法。这里来说一下一些重要的不同之处:
1. 寄存器写法
事先声明一下,如果不是用as
汇编,而是在 C 语言中或者其他情况下使用 clang 或者 gcc 来处理,那么不用管这条内容。
在 Windows 和 C 的汇编语法中,寄存器的名称是直接写的,例如mov ax,2
。但是在as
中,为了不与标识符(identifier,其实就是常说的变量)搞混,需要在寄存器前加上百分号%
,例如mov %ax,2
。并且所有的寄存器都得写成小写字母,不能像 Windows 或 C 里一样不分大小写。
十六进制写法
在 Windows 中,十六进制被写成以H
或h
结尾的,例如5c0dH
。但是在 Mac OS X 中,需要写成0x
开头的,例如0x1234
。而在 C 语言中,二者皆可。
来用汇编写一个 Hello World 吧!
写代码
最后会给出完整代码,现在先分步讲解。
首先新建一个名为helloworld.s
的文件,汇编代码文件的后缀一般是.asm
或者.s
。这时候就可以来写第一部分的内容啦。
先是需要指明第一部分是——可执行命令:
.section __TEXT,__text,regular,pure_instructions
然后指明创建的目标平台:
.build_version macos, 12, 0 sdk_version 12, 1
然后新建一个外部符号名称_main
用作程序的入口,就像 C 语言程序必须要有main()
函数一样。注意这里的下划线不可以省略。
.globl _main ## -- Begin function main
然后使用对齐(align)命令将位置计数器移到下一个边界4, 0x90
(一般来说都是地址)。
.p2align 4, 0x90
然后我们就可以开始写_main
部分包含的内容啦,也就是开始写main()
函数了:
_main: ## @main
.cfi_startproc ##表示函数的开头。会初始化一些内部的数据结构,发出架构依赖的初始CFI 指令
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc ##结束函数
这部分包含一些 cfi 相关的指令,可以在这里看到:https://web.mit.edu/rhel-doc/3/rhel-as-en-3/cfi-directives.html
此外,CFA是规范框架地址(Canonical Frame Address),相关资料可以看这里:
https://www.keil.com/support/man/docs/armasm/armasm_dom1361290010153.htm
接下来开始第二部分——可执行命令包含的字符串:
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "hello world!\n"
这里就是准备输出的字符串:hello world!\n
了。
如果这里使用的是.ascii
命令,那么这行应该改成.asciz "hello world!\n\0"
。
因为.asciz
自动补上了字符串末尾的\0
,如果被用于 C 程序的话,就使用这个。
最后我们可以加上这么一行:
.subsections_via_symbols
这行命令会告诉静态连接编辑器,这部分对象文件(object file)能被分割成几块。不过这里用不用无所谓,不影响结果。但是出于严谨可以加上。
完整代码如下:
.section __TEXT,__text,regular,pure_instructions
.build_version macos, 12, 0 sdk_version 12, 1
.globl _main ## -- Begin function main
.p2align 4, 0x90
_main: ## @main
.cfi_startproc ## 表示函数的开头。会初始化一些内部的数据结构,发出架构依赖的初始CFI 指令
## %bb.0:
pushq %rbp
.cfi_def_cfa_offset 16
.cfi_offset %rbp, -16
movq %rsp, %rbp
.cfi_def_cfa_register %rbp
subq $16, %rsp
movl $0, -4(%rbp)
leaq L_.str(%rip), %rdi
movb $0, %al
callq _printf
xorl %eax, %eax
addq $16, %rsp
popq %rbp
retq
.cfi_endproc ## 结束函数
.section __TEXT,__cstring,cstring_literals
L_.str: ## @.str
.asciz "hello world!\n"
.subsections_via_symbols
汇编成可执行文件
在 Windows 中,现在主流的可执行文件的格式是 PE(Portable Executable File Format),而其中的“Executable”缩写就是很多人熟知的 EXE。在 macOS 中,可执行文件被称为Mach-O executable file
。
如果使用file
命令来查看一个可执行文件的话就可以看到,这里我们查看 Xcode:
# 可执行文件
$ file /Applications/Xcode.app/Contents/MacOS/Xcode
Xcode: Mach-O universal binary with 2 architectures: [x86_64:Mach-O 64-bit executable x86_64] [arm64:Mach-O 64-bit executable arm64]
Xcode (for architecture x86_64): Mach-O 64-bit executable x86_64
Xcode (for architecture arm64): Mach-O 64-bit executable arm64
所以最终目标是要把hello.s
这个汇编文件,“变”成一个Mach-O executable file
。
这里有两种方法:
方法 1
由于这个程序十分简单,也没有链接特殊的库,可以直接使用以下命令来:
$ gcc -nostartfiles helloworld.s
$ chmod 755 a.out
$ file a.out
a.out: Mach-O 64-bit executable x86_64
这时候可以看到,a.out
便是需要的Mach-O executable file
。
运行一下看看:
$ ./a.out
hello world!
非常不错。
方法 2
这种方法是常规做法,首先使用as
来汇编代码,并且修改权限:
$ as hello.s
$ chmod 755 a.out
这里需要注意一点,as
生成的是 Mach-O 对象文件(Mach-O object file),而不是Mach-O executable file
,是不能直接运行的,如果直接运行会提示cannot execute binary file
。在 Windows 中,对象文件被称为 COFF(Common Object File Format)。
这时候需要使用连接器ld
来处理对象文件。但是,如果简单的使用ld
会出现以下情况:
$ ld a.out
Undefined symbols for architecture x86_64:
"_printf", referenced from:
_main in a.out
ld: symbol(s) not found for architecture x86_64
这是因为使用了 C 标准库的_printf
,但是ld
默认搜不到。所以加上库地址就可以,如下:
$ ld a.out -o hello -lSystem -L/Library/Developer/CommandLineTools/SDKs/MacOSX.sdk/usr/lib
特别注意!一般Mach-O executable file
是不加任何后缀的。
这时候得到的hello
便是需要的Mach-O executable file
。使用file
命令来检查一下:
$ file hello
hello: Mach-O 64-bit executable x86_64
然后运行一下看看:
$ ./hello
hello world!
运行完美~
结尾
本文还会时不时更新一下,修改一些问题,添加一些内容。
总之,希望能帮到有需要的人~
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)