在汇编语言中使用C库函数
在 GNU 汇编: 第一个汇编程序 中, 利用Linux 系统调用将读到的 cpuid 显示到控制台上, 还有不使用系统调用的其他方法, 其中一种就是使用 C 库函数.
实例
demo.s
.section .data
output:
.asciz "The processor Vender ID is '%s'\n"
.section .bss
.lcomm buffer, 12
.section .text
.globl _start
_start:
// 获取 CPU ID
movl $0, %eax
cpuid
// 将 CPU ID 填充到 buffer
movl $buffer, %edi
movl %ebx, (%edi)
movl %edx, 4(%edi)
movl %ecx, 8(%edi)
// 调用C标准库 printf
pushl $buffer
pushl $output
call printf
addl $8, %esp
push $0
// 退出程序
call exit
注意这里使用的是
.asciz
命令而不是.ascii
, 这是因为 C 语言的字符串需要以空字符结尾来判断字符串的结束.命令
.comm
和.lcomm
用来声明通用内存区域,lcomm
表示该区域数据是静态的, 本地汇编代码之外的程序段无法访问该内存区域. 这里声明了 12 个字节的本地通用内存用于保存读取到的 cpuid.为了把参数传给 C 函数 printf, 必须把它们压入堆栈. 这是使用
pushl
指令完成的. 参数放入堆栈的顺序和 printf 函数获取它们的顺序是相反的, 所以buffer
先被放入, 然后是输出字符串, 在这些操作完成之后, 使用call
指令调用 printf 函数.指令
addl $8, %esp
将堆栈指针 sp 加 8, 其目的是为了清空调用 printf 函数时放入堆栈的参数, 使用相同的技术把返回值 0 压入堆栈供 C 函数 exit 使用.
连接 C 库函数
在 Linux 把 C 函数链接到汇编语言程序有两种办法:
- 静态链接
静态链接把目标代码直接复制到应用程序的可执行文件中, 这样会使可执行文件巨大, 并且如果同时运行程序的多个实例, 因为每个实例都有自己相同函数的拷贝,这会造成内存浪费. - 动态链接
动态链接使用库的方式使程序员可以在应用程序中引用函数, 但是不会把代码拷贝到可执行文件中. 在程序运行时由操作系统调用动态链接库, 并且多个程序可以共享动态链接库.
-
动态链接
在 Linux 中, 标准的 C 动态库位于 libc.so.x 文件中, 其中 x 代表 库的版本. 并且库文件默认被放置于 /lib 目录下.
我们可以使用 find 命令来查看 /lib 目录下的 C 库
$ find /lib/ -name "libc.so*" /lib/i386-linux-gnu/libc.so.6 /lib/x86_64-linux-gnu/libc.so.6
可以看到我的计算机中的 C 库版本是 6, 而且同时包含有 32 位和 64 位的版本.
-
编译链接
为了链接到 libc.so 文件, 需要使用 ld 的 -l 参数, 使用 -l 参数时, 不需要指定完整的库名称, 链接器会自动在默认库目录中查找:$ as demo.s -32 $ ld ./a.out -o demo -m elf_i386 -lc $ ./demo ./demo: No such file or directory
链接程序没有问题, 但是当我试图运行产生的可执行文件时, 出现了上述错误.
这是由于 Linux 系统加载一个包含动态链接的应用程序时,OS 将控制权传递给动态加载器而不是应用程序的正常入口点。动态加载器搜索并加载未解析的库,然后将控制权传递给应用程序的起始点。在 Linux 中动态加载器是ld-linux.so.2.
-
查看动态加载器
使用 ldd 命令可以查看一个程序的所使用的动态加载器和所依赖的库:$ ldd ./demo linux-gate.so.1 (0xf7f5d000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7d52000) /usr/lib/libc.so.1 => /lib/ld-linux.so.2 (0xf7f5f000)
可以看到在默认情况下
ld
错误的把/usr/lib/libc.so.1
作为动态加载器 ld-linux.so.2.为了找到正确的 ld-linux.so.2 加载器的位置, 再次使用 find 对关键字进行查找:
$ find /lib/ -name "ld-linux*" /lib/i386-linux-gnu/ld-linux.so.2 /lib/x86_64-linux-gnu/ld-linux-x86-64.so.2 /lib/ld-linux.so.2
可以看到本地目录中有 32 位和 64 位动态加载器
-
在 ld 命令中指定动态加载器
$ ld ./a.out -o demo -m elf_i386 -lc -dynamic-linker /lib/i386-linux-gnu/ld-linux.so.2 $ ldd ./demo linux-gate.so.1 (0xf7fbd000) libc.so.6 => /lib/i386-linux-gnu/libc.so.6 (0xf7db2000) /lib/i386-linux-gnu/ld-linux.so.2 => /lib/ld-linux.so.2 (0xf7fbf000) $ ./demo The processor Vender ID is 'GenuineIntel'
这里将程序所依赖的动态加载器指定为 32 位动态加载器, 该加载器会在程序运行前查找并加载 32 位动态链接库目录下的动态链接库. 最后程序输出了正确的结果.
使用 GCC 编译
也可以使用 gcc 编译器进行汇编和链接, 实际上, 对于本例子来说, 这会容易许多, 因为 gcc 编译器会自动链接必须的 C 库, 会自动设置正确的动态加载器.
- gcc 编译
注意把 _start 标签改为 main, 因为 gcc 通过 main 标签来识别入口地址而不是 _start:$ gcc demo.s -m32 $ ./demo The processor Vender ID is 'GenuineIntel'