百度离职后,随着递归式学习的深入,我的涉猎从 Lisp/Scheme/SICP,到 APUE,最近又转入到了汇编语言上。老实说,汇编这种文物级的东西,在一般的计算机编程中是绝难碰到的。但是计算机工程学到一定层次,瓶颈所在,就会发现,总是有那么几样东西,诸如汇编、C、 算法、编译原理、体系结构、操作系统、网络数据库等,这些基础知识绕不过,躲不开,而能否跨越这些坎,从某种意义上决定了一个程序员能否从合格到优秀、从优秀到卓越。

最早接触汇编还是在大一上一门计算机硬件基础的导论课上,我原以为这课既然叫计算机硬件基础,讲的应该都是软驱硬盘主板鼠标等如何拆卸组装选购的科普杂事。要知道,那个时候的我刚刚学会在 Windows XP 控制面板里面装卸软件。给我们讲课的是一个老头,用的是自己编写的教材。老头最津津乐道的事情是在 90 年代中期自己花了 400 块钱买了一个 32M 的 U 盘,却因为 U 盘容量太大而束手无策,不知道该存些啥东西。这课开始几次讲的确实是硬盘、鼠标组成科普原理啥的,后来讲着讲着就拐到了 16-bit DOS 下的 Debug 汇编编程, mov ax, bx 这种,结果可想而知,我们全班同学一下子就晕的不行,弃考了一小半,剩余勉强坚持到最后的,又挂了好几个。

大二下又上了一门汇编语言的通识课,仍旧是 16-bit DOS,仍旧是不得要领。直到大三,上了专业的汇编课,硬着头皮,在 Linux + DOSEMU + MASM 5.0 的环境下编了一个 300 多行的软件模拟浮点数算数指令的汇编程序,这才对 16-bit 汇编中的一些基本概念诸如段、 寻址方式、中断等等,有了一个初步的了解。不过说实话,大学里的汇编学习和实际脱节太远,多数的汇编教育还停留在 16-bit DOS 环境下,用得教材以清华的那本《IBM-PC汇编语言程序设计》居多,做得实验也都是 Window 98 DOS 子系统下步进电机、控制电灯明暗这种实验,一到实际编程,32-bit 和 64-bit 的保护模式下编程,加上复杂的 MMX 指令,浮点数指令,就傻眼了。

本文介绍 Linux 平台下 32-bit 的GAS汇编语言编程,GAS是 GCC 编译器的汇编器。之所以采用这套工具,主要的原因在于 GCC 工具链非常成熟,GAS配合 Emacs、GDB,加上 Shell 命令行,和 objdump、od 等工具,对于我这个 Linux fans 和命令行控有莫大的吸引力,工作效率自不必说,谁用谁知道。

x86 汇编编程有两种截然不同的语法风格,我们常见的是 Intel 语法风格,类似这种:

而 GAS 汇编采用的是 AT&T 的语法风格:

讲 GAS 汇编的书籍并不多,以下两本都是好书:

我粗略的看完了第一本,第二本正在进行中,这样的学习已经让我对 C 语言中很多高级的概念,诸如指针、编译连接过程、内存布局和分布有了比较深入的理解。

不过对于计算机的学习,光说不练是远远不够的。书本上看懂并不代表就能快速写出准确、 实用、高效的程序来。我学习一门计算机语言,往往都会去找一本评价较好的书籍,把书上所有的示例代码看懂,自己亲自敲一遍。这不,刚刚起步,就碰到了两个小问题,记录下来,作为 GAS 汇编学习的起点。

首先是 Emacs 编辑器的问题。Emacs 本身对汇编语言编辑功能的支持远比不上对其余高级编程语言的支持。一个内建的 ASM Mode,乃是 ESR 的大作,粗略看了下,两百多行的代码,功能有限,要命的是还有个小 bug,那就是默认的注释符号为 ; ,而不是 GAS 汇编接受的 # 。恰好这个月花费了大量精力研习了 Emacs Manual 和 Elisp Manual,因此不自量力写了两个小函数,专门用于 GAS 汇编语言的注释(代码很初级,elisp 高手轻拍):

希望随着学习 Emacs 和 Elisp 的深入,自己能够写出一个稍微好一点的 GAS 汇编的 major-mode(顺便,前两天给 Emacs 的包管理工具 el-get 提供了一 rcp 被接受了,这是我认真学习计算机以来第一次为开源项目做出点实际的贡献,开心)。

第二个问题是 64-bit 下写 32-bit 汇编程序的问题。这其中涉及到两个问题。其一是 32-bit 汇编和 64-bit 汇编的内在区别问题,包括语法层面上的,和机器层面上的,这个我还没有发言权,这里有一篇文档,详述了 x86 下 64-bit 汇编和 32 汇编的区别。而由于我上面提到的两本好书中,采用的都是 32-bit 的汇编,而 32-bit 向 64-bit 的迁移并不是无缝的,外加上我现在对 64-bit 汇编了解有限,因此如何在 64-bit 平台下编写、汇编、连接 32-bit 汇编,就是我们要解决的第二个问题。

要解决这个问题,就要对 C 程序的整个编译流程有一个稍微详细点的了解,光靠编译器点击一个按钮自动编译,是解决不了这个问题的。这个我不再具体展开,《程序员的自我修养》是这方面难得的一本好书,我粗略看过一遍,但一直不敢在豆瓣打上“看过”的标签,因为没有对计算机体系结构、汇编语言和 C 语言的深刻了解,是不可能深刻理解并“看过”此书的。

下面给出一个示例程序,来自于《Professional Assembly Language》

这个程序很简单,主要是调用 cpuid 指令得到 CPU 本身的一些信息,然后调用 C 标准库中的函数打印出来,之后利用 Linux 系统调用 write 打印出一个字符串,最后再次调用 C 标准库中的 exit 函数,状态码为 1。我们来验证下此程序的编译、连接和运行过程,主要的汇编、连接指令是:

  • sudo pacman -S gcc-multilib binutils-multilib gcc-libs-multilib lib32-glibc
    • 安装必须的 32-bit 编译器和运行库
  • as -g -o cpuid2.o cpuid2.s --32
    • --32 – 生成 32-bit 的 .o 文件
    • -g – 生成 GDB 调试信息,便于程序的调试
  • ld --dynamic-linker /lib/ld-linux.so.2 cpuid2.o -o cpuid2 -m elf_i386 -L/usr/lib32 -lc
    • --dynamic-linker /lib/ld-linux.so.2 – 采用动态连接
    • -m elf_i386 – 生成 32-bit 的程序
    • -L – 讲 lib32-glibc 的库加入库搜索路径
    • -lc – 连接标准 C 语言库, printf 必须
% uname -m
x86_64
% as -g -o cpuid2.o cpuid2.s --32
% file cpuid2.o
cpuid2.o: ELF 32-bit LSB relocatable, Intel 80386, version 1 (SYSV), not stripped
% ld --dynamic-linker /lib/ld-linux.so.2  cpuid2.o -o cpuid2 -m elf_i386 -L/usr/lib32 -lc
% file cpuid2
cpuid2: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), dynamically linked (uses shared libs), not stripped
% ./cpuid2
The processor Vendor ID is 'GenuineIntel'
The processor Vendor ID is '%s'
% echo $?
1
% ls -l
total 24
drwxr-xr-x  2 lox users 4096 Mar 24 12:07 .
drwxr-xr-x 19 lox users 4096 Feb 29 20:08 ..
-rwxr-xr-x  1 lox users 2675 Mar 24 12:07 cpuid2
-rw-r--r--  1 lox users 1464 Mar 24 12:07 cpuid2.o
-rw-r--r--  1 lox users  414 Mar 23 18:18 cpuid2.s
-rw-r--r--  1 lox users  348 Mar 23 10:46 cpuid.s
%

OK,就这么多,至于 Makefile、GAS 程序调试等话题,容我后续再叙。