泰晓科技 -- 聚焦 Linux - 追本溯源,见微知著!
网站地址:https://tinylab.org

泰晓Linux知识星球:1300+知识点,520+用户
请稍侯

Linux 汇编语言快速上手:4大架构一块学

Wu Zhangjin 创作于 2015/05/20

By Falcon of TinyLab.org 2015/05/13

前言

万事开头难。如果初次接触,可能会觉得汇编语言很难下手。但现如今,学习汇编语言非常方便,本文就此展开。

实验环境

早期学习汇编语言困难,有很大一个原因是没有合适的实验环境:

  • 没有钱买开发板
  • 找不到合适的开发板
  • 有了开发板跑起来也没那么容易

现在学汇编语言根本不需要开发板,可以用 qemu-user-static 直接运行各种架构的汇编语言。

以 Ubuntu 为例,Windows 和 Mac 下的用户可以先安装 VirtualBox + Ubuntu,再安装这个。

sudo apt-get install qemu-user-static

接着安装 gcc。

sudo apt-get install gcc
sudo apt-get install gcc-arm-linux-gnueabi gcc-aarch64-linux-gnu
sudo apt-get install gcc-powerpc-linux-gnu gcc-powerpc64le-linux-gnu

因为 Ubuntu 自带的交叉编译工具不支持 Mips 平台,所以需要额外安装。但是 emdebian.org 从 2014 年以后不再维护,已经无法直接安装。

如果要完整地做完后续实验,强烈推荐使用泰晓科技的 Linux Lab 实验环境,该环境自带了本文所需的交叉编译工具。下面的所有例子都上传到了该环境的 examples/assembly/ 目录下。

Hello World

同大多数资料一样,我们也从 Hello World 入手。

学习一个东西比较高效的方式是照猫画虎,咱们先直接从 C 语言生成一个汇编语言程序。

C 语言版本

先写一个 C 语言的 hello.c

    #include <stdio.h>

    int main(int argc, char *argv[])
    {
            printf("Hello World\n");

            return 0;
    }

汇编语言版本

生成汇编语言:

gcc -S hello.c

默认会生成 hello.s,可以用 -o hello-x86_64.s 指定输出文件名称。

gcc -S hello.c -o hello-x86_64.s

下面类似地,列出所有 4 个平台 32位 和 64位 汇编语言生成办法。

  • X86

    gcc -m32 -S hello.c -o hello-x86.s
    gcc -S hello.c -o hello-x86_64.s
    
  • MIPS

    mipsel-linux-gnu-gcc -S hello.c hello-mips.s
    mipsel-linux-gnu-gcc -mabi=64 -S hello.c -o hello-mips64.s
    
  • ARM

    arm-linux-gnueabi-gcc -S hello.c -o hello-arm.s
    aarch64-linux-gnu-gcc -S hello.c -o hello-arm64.s
    
  • PowerPC

    powerpc-linux-gnu-gcc -S hello.c -o hello-powerpc.s
    powerpc64le-linux-gnu-gcc -S hello.c -o hello-powerpc64.s
    

我们就这样轻松地获得了所有平台的第一个可以打印 Hello World 的汇编语言程序:hello-xx.s。

大家可以用 vim 等编辑工具打开这些文件试读,读不懂也没关系,我们下一节会结合后续的参考资料做进一步分析。

编译汇编语言程序

在进一步分析前,我们演示如何把汇编语言编译成可执行文件。

静态编译

如果要直接在当前系统中运行,简便起见,需要把各类库静态编译进去(X86实际不需要,因为主机本身就是X86平台),可以这么做:

  • X86

    gcc -m32 -o hello-x86 hello-x86.s -static
    gcc -o hello-x86_64 hello-x86_64.s -static
    
  • MIPS

    mipsel-linux-gnu-gcc -o hello-mips hello-mips.s -static
    mipsel-linux-gnu-gcc -mabi=64 -Wl,-melf64ltsmip -o hello-mips64 hello-mips64.s -static  # Not work ?
    
  • ARM

    arm-linux-gnueabi-gcc -o hello-arm hello-arm.s -static
    aarch64-linux-gnu-gcc -o hello-arm64 hello-arm64.s -static
    
  • PowerPC

    powerpc-linux-gnu-gcc -o hello-powerpc hello-powerpc.s -static
    powerpc64le-linux-gnu-gcc -o hello-powerpc64 hello-powerpc64.s -static # Not work ?
    

动态编译

静态编译的缺点是把所有用到的库都默认编译进了可执行文件,会导致编译出来的可执行文件占用较多磁盘,而且在运行时占用更多内存。

所以可以考虑用动态编译。动态编译与静态编译的区别是,动态编译需要有动态库装载和链接器:ld.so 或者 ld-linux.so,这个工具的路径默认在 /lib 下。例如:

$ ldd hello-x86
linux-gate.so.1 =>  (0xf76ea000)
libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7508000)
/lib/ld-linux.so.2 (0xf76eb000)
$ mipsel-linux-gnu-readelf -l hello-mips | grep interpreter
  [Requesting program interpreter: /lib/ld.so.1]

所以,除了 x86 以外,对于相关库都安装在非标准路径下,所以动态编译或者运行时,其他架构需要明确指定库的路径。先通过如下命令获取 ld.so 的安装路径:

$ dpkg -L libc6-mipsel-cross | grep ld.so
/usr/mipsel-linux-gnu/lib/ld.so.1

发现所有库都安装在 /usr/ARCH-linux-gnu[eabixx]/lib/ 下面,所以,可以这么执行:

$ LD_LIBRARY_PATH=/usr/mipsel-linux-gnu/lib/
$ qemu-mipsel $LD_LIBRARY_PATH/ld.so.1 --library-path $LD_LIBRARY_PATH ./hello-mips

或者

$ qemu-mipsel -E LD_LIBRARY_PATH=$LD_LIBRARY_PATH $LD_LIBRARY_PATH/ld.so.1 ./hello-mips

通过上面的方法在 x86 下执行其他架构的程序确实不方便,不过比买开发板划算多了吧。何况咱们还可以写个脚本来替代上面的一长串的命令。

实际上咱们可以更简化一些,可以在编译时指定 ld.so 的全路径:

$ mipsel-linux-gnueabi-gcc -Wl,--dynamic-linker=/usr/mipsel-linux-gnueabi/lib/ld.so.1 -o hello hello.c
$ readelf -l hello | grep interpreter
  [Requesting program interpreter: /usr/arm-linux-gnueabi/lib/ld-linux.so.3]
$ qemu-mipsel -E LD_LIBRARY_PATH=$LD_LIBRARY_PATH ./hello-mips

不过这种方法也不是那么靠谱。

可选的办法是,用 debootstrap 安装一个完整的支持其他架构的文件系统,然后把 /usr/bin/qemu-XXX-static 拷贝到目标文件系统的 /usr/bin 下,然后 chroot 过去使用。这里不做进一步介绍了。

汇编语言分析

上面介绍了如何快速获得一个可以打印 Hello World 的汇编语言程序。不过咋一看,简直是天书。

作为快速上手,咱们也没有过多篇幅来介绍太多的背景,因为涉及的背景实在太多。会涉及到:

这些内容是不可能在几百文字里头描述清楚的,所以干脆跳过交给同学们自己参考后续资料后再回过头来阅读。咱们进入下一节,看看更简单的实现。

进阶学习

如果是简单打印 Hello World,咱们其实可以不用调用库函数,可以直接调用系统调用 sys_writesys_write 是一个标准的 Posix 系统调用,各平台都支持。参数完全一致,不过各平台的系统调用号可能有差异:

ssize_t write(int fd, const void *buf, size_t count);

系统调用号基本都定义在:arch/ARCH/include/asm/unistd.h。例如:

$ grep __NR_write -ur arch/mips/include/asm/
arch/mips/include/asm/unistd.h:#define __NR_write           (__NR_Linux +   4)

而 __NR_Linux 为 4000:

 $ grep __NR_Linux -ur arch/mips/include/asm/ -m 1
 arch/mips/include/asm/unistd.h:#define __NR_Linux          4000

所以,在 MIPS 上,系统调用号为 4004,具体看后面的例子。

下面来看看简化后的例子,例子全部摘自后文的参考资料。

X86

X86 32Bit

.data                   # section declaration
msg:
    .string "Hello, world!\n"
    len = . - msg   # length of our dear string
.text                   # section declaration
                        # we must export the entry point to the ELF linker or
    .global _start      # loader. They conventionally recognize _start as their
                        # entry point. Use ld -e foo to override the default.
_start:
# write our string to stdout
    movl    $len,%edx   # third argument: message length
    movl    $msg,%ecx   # second argument: pointer to message to write
    movl    $1,%ebx     # first argument: file handle (stdout)
    movl    $4,%eax     # system call number (sys_write)
    int     $0x80       # call kernel
# and exit
    movl    $0,%ebx     # first argument: exit code
    movl    $1,%eax     # system call number (sys_exit)
    int     $0x80       # call kernel

编译和链接:

$ as --32 -o x86-hello.o x86-hello.s
$ ld -melf_i386 -o x86-hello x86-hello.o

X86 64Bit

.global _start
.text
_start:
    # write(1, message, 13)
    mov     $1, %rax                # system call 1 is write
    mov     $1, %rdi                # file handle 1 is stdout
    mov     $msg, %rsi              # address of string to output
    mov     $len, %rdx              # number of bytes
    syscall                         # invoke operating system to do the write

    # exit(0)
    mov     $60, %rax               # system call 60 is exit
    xor     %rdi, %rdi              # we want return code 0
    syscall                         # invoke operating system to exit
.data
msg:
    .ascii  "Hello, world\n"
    len = . - msg   # length of our dear string

编译和链接:

$ as -o x64-hello.o x64-hello.s
$ ld -o x64-hello x64-hello.o

MIPS

mipsel 32Bit

# File: hello.s -- "hello, world!" in MIPS Assembly Programming
# by falcon <wuzhangjin@gmail.com>, 2008/05/21
# refer to:
#    [*] http://www.tldp.org/HOWTO/Assembly-HOWTO/mips.html
#    [*] MIPS Assembly Language Programmer’s Guide
#    [*] See MIPS Run Linux(second version)
# compile:
#       $ as -o hello.o hello.s
#       $ ld -e main -o hello hello.o

# data section
.rdata
hello: .asciiz "hello, world!\n"
length: .word . - hello            # length = current address - the string address

# text section
.text
.globl main
main:
    # if compiled with gcc-4.2.3 in 2.6.18-6-qemu the following three statements are needed

    .set noreorder
    .cpload $t9
    .set reorder

            # there is no need to include regdef.h in gcc-4.2.3 in 2.6.18-6-qemu
            # but you should use $a0, not a0, of course, you can use $4 directly

            # print "hello, world!" with the sys_write system call,
            # -- ssize_t write(int fd, const void *buf, size_t count);
    li $a0, 1    # first argumen: the standard output, 1
    la $a1, hello    # second argument: the string addr
    lw $a2, length  # third argument: the string length
    li $v0, 4004    # sys_write: system call number, defined as __NR_write in /usr/include/asm/unistd.h
    syscall        # causes a system call trap.

            # exit from this program via calling the sys_exit system call
    move $a0, $0    # or "li $a0, 0", set the normal exit status as 0
            # you can print the exit status with "echo $?" after executing this program
    li $v0, 4001    # 4001 is __NR_exit defined in /usr/include/asm/unistd.h
    syscall

编译和链接:

$ mipsel-linux-gnu-as -o mipsel-hello.o mipsel-hello.s
$ mipsel-linux-gnu-ld -o mipsel-hello mipsel-hello.o

mipsel 64Bit

# data section
.rdata
hello: .asciiz "hello, world!\n"
length: .word . - hello            # length = current address - the string address

# text section
.text
.globl __start
__start:
    # If compiled with gcc-4.2.3 in 2.6.18-6-qemu the following three statements are needed
    # in compiling relocatable code, to follow the PIC-ABI calling conventions and other protocols.
    .set noreorder
    .cpload $gp
    .set reorder

    # There is no need to include regdef.h in gcc-4.2.3 in 2.6.18-6-qemu
    # but you should use $a0, not a0, of course, you can use $4 directly
    # print "hello, world!" with the sys_write system call,
    # -- ssize_t write(int fd, const void *buf, size_t count);

    li $a0, 1       # first argument: the standard output, 1
    dla $a1, hello   # second argument: the string addr
    lw $a2, length  # third argument: the string length
    li $v0, 5001    # sys_write: system call number, defined as __NR_write in /usr/include/asm/unistd.h
    syscall         # causes a system call trap.
                    # exit from this program via calling the sys_exit system call
    move $a0, $0    # or "li $a0, 0", set the normal exit status as 0
                    # you can print the exit status with "echo $?" after executing this program
    li $v0, 5058    # 4001 is __NR_exit defined in /usr/include/asm/unistd.h
    syscall

编译和链接:

$ mipsel-linux-gnu-as -mabi=64 -o mips64el-hello.o mips64el-hello.s
$ mipsel-linux-gnu-ld -m elf64ltsmip -o mips64el-hello mips64el-hello.o

运行:

$ qemu-mips64el ./mips64el-hello

备注:由于 qemu-mips64el 的 binfmt 默认并没有注册到 /proc/sys/fs/binfmt_misc/,所以生成的文件无法直接执行,需要额外注册如下:

$ sudo bash -c "echo ':mips64el:M::\x7fELF\x02\x01\x01\x00\x00\x00\x00\x00\x00\x00\x00\x00\x02\x00\x08\x00:\xff\xff\xff\xff\xff\xff\xff\x00\xff\xff\xff\xff\xff\xff\xff\xff\xfe\xff\xff\xff:/usr/bin/qemu-mips64el:' > /proc/sys/fs/binfmt_misc/register"

具体可参考:qemu-binfmt-confAnalyzingReversingAndEmulatingFirmware

ARM

ARM32

.data

msg:
    .ascii      "Hello, ARM!\n"
len = . - msg


.text

.globl _start
_start:
    /* syscall write(int fd, const void *buf, size_t count) */
    mov     %r0, $1     /* fd -> stdout */
    ldr     %r1, =msg   /* buf -> msg */
    ldr     %r2, =len   /* count -> len(msg) */
    mov     %r7, $4     /* write is syscall #4 */
    swi     $0          /* invoke syscall */

    /* syscall exit(int status) */
    mov     %r0, $0     /* status -> 0 */
    mov     %r7, $1     /* exit is syscall #1 */
    swi     $0          /* invoke syscall */

编译和链接:

$ arm-linux-gnueabi-as -o arm-hello.o arm-hello.s
$ arm-linux-gnueabi-ld -o arm-hello arm-hello.o

ARM64

.text //code section
.globl _start
_start:
    mov x0, 0     // stdout has file descriptor 0
    ldr x1, =msg  // buffer to write
    mov x2, len   // size of buffer
    mov x8, 64    // sys_write() is at index 64 in kernel functions table
    svc #0        // generate kernel call sys_write(stdout, msg, len);

    mov x0, 123 // exit code
    mov x8, 93  // sys_exit() is at index 93 in kernel functions table
    svc #0      // generate kernel call sys_exit(123);

.data //data section
msg:
    .ascii      "Hello, ARM!\n"
len = . - msg

编译和链接:

aarch64-linux-gnu-as -o aarch64-hello.o aarch64-hello.s
aarch64-linux-gnu-ld -o aarch64-hello aarch64-hello.o

PowerPC

PPC32

.data                       # section declaration - variables only
msg:
    .string "Hello, world!\n"
    len = . - msg       # length of our dear string
.text                       # section declaration - begin code
    .global _start
_start:
# write our string to stdout
    li      0,4         # syscall number (sys_write)
    li      3,1         # first argument: file descriptor (stdout)
                        # second argument: pointer to message to write
    lis     4,msg@ha    # load top 16 bits of &#038;msg
    addi    4,4,msg@l   # load bottom 16 bits
    li      5,len       # third argument: message length
    sc                  # call kernel
# and exit
    li      0,1         # syscall number (sys_exit)
    li      3,1         # first argument: exit code
    sc                  # call kernel

编译和链接:

$ powerpc-linux-gnu-as -o ppc32-hello.o ppc32-hello.s
$ powerpc-linux-gnu-ld -o ppc32-hello ppc32-hello.o

PPC64

.data                       # section declaration - variables only
msg:
    .string "Hello, world!\n"
    len = . - msg       # length of our dear string
.text                       # section declaration - begin code
        .global _start
        .section        ".opd","aw"
        .align 3
_start:
        .quad   ._start,.TOC.@tocbase,0
        .previous
        .global  ._start
._start:
# write our string to stdout
    li      0,4         # syscall number (sys_write)
    li      3,1         # first argument: file descriptor (stdout)
                        # second argument: pointer to message to write
    # load the address of 'msg':
                        # load high word into the low word of r4:
    lis 4,msg@highest   # load msg bits 48-63 into r4 bits 16-31
    ori 4,4,msg@higher  # load msg bits 32-47 into r4 bits  0-15
    rldicr  4,4,32,31   # rotate r4's low word into r4's high word
                        # load low word into the low word of r4:
    oris    4,4,msg@h   # load msg bits 16-31 into r4 bits 16-31
    ori     4,4,msg@l   # load msg bits  0-15 into r4 bits  0-15
    # done loading the address of 'msg'
    li      5,len       # third argument: message length
    sc                  # call kernel
# and exit
    li      0,1         # syscall number (sys_exit)
    li      3,1         # first argument: exit code
    sc                  # call kernel

编译和链接:

$ powerpc-linux-gnu-as -a64 -o ppc64-hello.o ppc64-hello.s
$ powerpc-linux-gnu-ld -melf64ppc -o ppc64-hello ppc64-hello.o

小结

到这里,四种主流处理器架构的最简汇编语言都玩转了,接下来就是根据后面的各类参考资料,把各项基础知识研究透彻吧。

参考资料

书籍

  • X86: x86/x64 体系探索及编程
  • ARM: ARM System Developers’ Guide: Designing and Optimizing System Software
  • MIPS: See MIPS Run Linux
  • PowerPC: PowerPC™ Microprocessor Common Hardware Reference Platform: A System Architecture

指令手册

课程/文章



Read Related:

Read Latest: