在OS X上玩x86_64汇编: Day 1

OS X提供了和Unix兼容的汇编语言,是基于AT&T语法的,和早先更广为流传的NASM汇编器所使用的Intel语法有很多不一样的地方。废话少说,先上代码,第一天我们做一件很简单的事,就是在Terminal里输入一些字符,然后再原样输出。


1. 先写代码

我们的第一段代码要设置几个变量,它们是系统调用(syscall)的代号,用来实现键盘输入和屏幕输出,以及在进程退出时向内核发送正确的信号,而不是让内核以为这个程序是出错才退出的。调用它们有点像调用函数,但是和使用C标准库的printf和scanf又不太一样,它们叫做FreeBSD System call,顾名思义是从FreeBSD继承来的。我们马上会看到要如何使用它。

.set SyscallExit,    0x2000001
.set SyscallDisplay, 0x2000004
.set SyscallRead,    0x2000003

第二段要开辟出一个空间,来存放键盘输入的内容

.section __DATA, __data
InputBufferLength:
    .quad  0
InputBuffer:
    .fill  64, 1, 0x20 
InputBufferEnd:

其中.section __DATA, __data这句话的意思是把这部分代码放进初始化的代码段里。按照x86的汇编风格,一个完整二进制代码分为若干段,有的放指令,有的放数据。对于初始化的数据,值是直接写进二进制代码中的,而不是在二进制代码运行的时候才写入。另外还有.bss段,是未初始化的代码,那么C语言中的static变量如果没有赋值,变量的地址都会被安排在.bss段里。

凡从一行开始忽略起始空格,以字母开头冒号结尾的都叫做语句标号,它实际指向一个地址,譬如上面代码中的InputBufferLength就是一个地址。这个标号不会出现在指令中,也不占用代码的空间(这么说不严谨,因为出于调试的需要,标号和地址的对应关系默认会保存在二进制文件里,但是在编译时可以选择去掉来减少二进制的大小,并不会影响运行)

.quad 0是编译器宏,表示生成8个字节长的数字,而它的值是0。所谓宏就是编译时的逻辑,只是针对二进制本身的操作,不会影响到二进制的运行时。编译器的宏非常强大,它本身是一套图灵完备的语言,而它操作的对象是未来要运行的二进制代码。其实有点像HTML的模版语言,如jekyll或EJS。未来我们还会接触到更强大的宏。最起初的.set指令也是宏,SyscallDisplay等名称也不会保存在二进制中。

下一部分是InputBuffer是实际的记录键盘输入的区域,.fill代表在接下来的64次重复中,把0x20这个值填入1个字节里。

最后一个标号不指向数据,而是用来计算buffer长度。另外语句表号其实很大程度上可以代替注释的功能,所以要善于使用。那么我们所需的数据就到此为止,接下来是代码段。

.section __TEXT, __text
.globl _main

由于我们使用gcc (llvm-gcc)来编译,main是默认的程序入口名称。所以我们把`_main声明为一个全局标签,这样编译器就会去找那个_main的标签,以它作为程序的入口逐条执行指令。

接下来是两段宏定义,让大家久等了,我们终于见到了实际的代码。然而需要注意的是,严格来说它们仍不是实际的代码。因为如果不引用这些定义,它们不会出现在编译后的二进制中。

.macro Print
    movq    $SyscallDisplay, %rax
    movq    $(1), %rdi
    leaq    InputBuffer(%rip), %rsi
    movq    InputBufferLength(%rip), %rdx
    syscall
.endm

.macro ScanInputBuffer
    movq    $SyscallRead, %rax
    movq    $(1), %rdi
    leaq    InputBuffer(%rip), %rsi
    movq    $(InputBufferEnd - InputBuffer), %rdx
    syscall

    movq    %rax, InputBufferLength(%rip)
.endm

在以上两段宏定义中,我们看到了最初定义的syscall是如何使用的。在编译时它们会被替换为那些数字。所有%开头的都是寄存器,就是在高级语言中不会直接接触的存储单元。x86_64有16个通用数据寄存器可以直接使用,具体可自行维基。还有很多更专一功能的寄存器,我们会在很久以后才会遇到。

Print做了这样几件事。当执行syscall时,它先看%rax寄存器中的编号来决定是哪个system call,%rdi中存储的是退出代码,放1代表结束call时是成功退出的。

接下来的两个内容比较关键,均涉及到牛逼而复杂的概念。第一个是leaq指令,它做的事情是将第一个argument的有效地址丢到%rsi里。我们刚才提到InputBuffer不是已经是个地址了么?我在这里需要声明一下,编译器会通过各种方式来使用这个地址,在不同场合它的值其实是不同的。

InputBuffer(%rip)是一个典型的offset(base-addr)的寻址方式,具体内容可参考这里。而在这里的特别之处是,%rip是指令指针寄存器,当汇编器遇到label(%rip)这种用法时,它不代表从段起始地址到标号的偏移,而是当前指令的地址到那个标号的偏移。因为%rip存储着当前指令的地址,所以%rip的地址加上偏移就能定位到那个标号所对应实际内存的地址。

movqleaq不同的是,它不是把地址丢进后面的寄存器,而是把地址上对应的内容丢进去。syscall指令把四个寄存器的内容作为参数,输出以%rsi所存内容为起始地址的,以%rdx所存内容为长度的字符串。对比来看,我们看到下面ScanInputBuffer中所使用的寄存器及用法也都是一样的。这里我们会遇到写汇编需要在头脑中保持清醒的事情,就是寄存器的数据不区分是数据还是地址,按着不同的寻址方式,寄存器的内容既可以按数据来使用,也可以按地址来使用。所以写代码的时候要保持头脑清醒。

最后我们终于进入了实际执行的代码:

_main:
    ScanInputBuffer
    Print

    movq $SyscallExit, %rax
    syscall

在这里我们像函数调用一样使用了两个宏定义,但事实上编译器做的工作是把代码插了进去。因此上面的宏定义内的代码对寄存器的影响会持续下去。最后我们使用了三个system call的最后一个,也就是退出。

好了,我们今天要完成的全部代码都在这里:

.set SyscallExit,       0x2000001
.set SyscallDisplay,     0x2000004
.set SyscallRead,       0x2000003

.section __DATA, __data
InputBufferLength:
    .quad   0
InputBuffer:
    .fill   64, 1, 0x20 
InputBufferEnd:

.section __TEXT, __text
.globl _main

.macro Print
    movq    $SyscallDisplay, %rax
    movq    $(1), %rdi
    leaq    InputBuffer(%rip), %rsi
    movq    InputBufferLength(%rip), %rdx
    syscall
.endm

.macro ScanInputBuffer
    movq    $SyscallRead, %rax
    movq    $(1), %rdi
    leaq    InputBuffer(%rip), %rsi
    movq    $(InputBufferEnd - InputBuffer), %rdx
    syscall

    movq    %rax, InputBufferLength(%rip)
.endm

_main:
    ScanInputBuffer
    Print

    movq $SyscallExit, %rax
    syscall

2.再写Makefile

我们先写一个简单的,日后再往进添加功能

all: Main.s
    cc  $^ -lc -o exor
clean:
    rm exor

3. 运行

这里包含了我们日后要往Makefile里添加的东西,可以先忽略。内容大致是代码段的代码和数据段的数据。



我们可以看到结果。

4.小结

我们今天看到了syscall的用法,看到了不同的寻址方式,以及一个完整的用汇编写一个程序的流程。这是我们接下来学习的基础,祝大家玩得开心。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 160,108评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,699评论 1 296
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,812评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,236评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,583评论 3 288
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,739评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,957评论 2 315
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,704评论 0 204
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,447评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,643评论 2 249
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,133评论 1 261
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,486评论 3 256
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,151评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,108评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,889评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,782评论 2 277
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,681评论 2 272

推荐阅读更多精彩内容