C 指针原理揭秘:基于底层实现机制 (17):AT&T 汇编概述 3.1.4

阅读数:5 2019 年 12 月 11 日 20:21

C指针原理揭秘:基于底层实现机制(17):AT&T汇编概述 3.1.4

(第一个 AT&T 汇编)

内容简介
全书分为准备篇、基础篇、揭秘篇、实战篇。本书力求从底层实现机制进行解析,同时配合 C/C++ 编程技巧以及某些指针运用技巧,讲解如何提高程序效能,如何避免滥用指针。
准备篇中介绍 C 指针概述、UBUNTU 及开发环境配置、AT&T 汇编简介、编译原理基础;基础篇将对 AT&T 汇编以及 C 指针基础进行介绍;揭秘篇讲述高级 C 指针的实现机制以及 C++ 指针实现机制,同时讲解编程技巧和 C/C++ 指针高级应用;实战篇讲解解释语言指针、TCC 编译实践、垃圾回收等高级 C 指针应用话题。

学习汇编最有效的方法就是动手实践,下面就来开始编写第一个汇编程序吧!本程序需要完成的功能是:将 66 与 20 相加,相加的结果(88)是字母“B”的 ASCII 码,将“B”与后面跟随的换行符(换行符的 ASCII 码为 10)一起输出到屏幕上。不同的操作系统中,汇编代码会稍有不同,下面将分别以 FreeBSD 与 Ubuntu 系统为例进行讲解。

  1. FreeBSD 系统

FreeBSD 系统可通过 Vim 或 ee 编辑文件 3-1.s,输入如程序 3-1 所示的代码:

程序 3-1 第一个汇编程序:输出“B”
复制代码
.section .data
output:
.byte 46
.byte 10
.section .text
.globl _start
_start:
movl $output,%edx
addl $20,(%edx)
pushl $2 # 参数三:字符串长度,包括换行符共 2 个字符
pushl $output # 参数二:要显示的字符串
pushl $1 # 参数一:文件描述符(stdout)
movl $4, %eax # 系统调用号(sys_write)
pushl %eax
int $0x80 # 调用内核功能显示字符及回车
pushl $0 # 参数一:退出代码
movl $1,%eax # 系统调用号(sys_exit)
int $0x80 # 调用内核功能

与 C 程序需要编译才能运行一样,汇编程序不能直接执行,也需要先进行汇编,并且要在链接后才能执行,具体过程如下。

首先,进行汇编:

复制代码
%as -o 3-1.o 3-1.s

然后,链接:

复制代码
%ld -o 3-1 3-1.o

最后,运行测试:

复制代码
% ./3-1
B

下面对程序 3-1 进行剖析,初步熟悉一下 AT&T 汇编。

1)AT&T 汇编代码通过.section 声明不同的段,程序 3-1 声明了两个段,它们分别是.section .data 段和.section .text 段。.section .data 段为数据段,用于存放可供汇编程序读写的数据;.section .text 段为代码段,用于存放汇编程序代码,程序在运行时会为这两个段分配相应大小的内存,当程序结束时,这些内存会被自动释放。

2)程序 3-1 中的数据段中存放了 1 个字节(byte)大小的数字 66 和同样大小的换行符(数字 10 表示换行符的 ASCII 码),代码片断如下:

复制代码
.section .data
output:
.byte 46
.byte 10

上述汇编代码完成的功能相当于下面这条 C 语句:

复制代码
unsigned char output[2]={60,10};

3)程序 3-1 的代码段的开头处有这样一条语句“globl _start”,这条语句标注了程序的起始点(相当于 C 语言的 main 函数)。globl 标记用于指示外部程序可访问的程序标签,_start 标签是 ld 链接器进行链接时默认程序的起始点,它们组合在一起的含义是:当汇编程序运行时,指令指针将指向 _start 标签处(即代码段的开头,第一条汇编代码处),从 _start 标签指向的汇编代码开始运行。注意,“_start”是默认的名字,如果要使用其他名字,则需要在链接时使用“-e”选项指定起始点名称。

4)紧接着 _start 标签的程序可分为如下两块。

第一块是显示字符“B”及换行符,代码片断如下:

复制代码
movl $output,%edx
addl $20,(%edx)
pushl $2 # 参数三:字符串长度,包括换行符共 2 个字符
pushl $output # 参数二:要显示的字符串
pushl $1 # 参数一:文件描述符(stdout)
movl $4, %eax # 系统调用号(sys_write)
pushl %eax
int $0x80 # 调用内核功能显示字符及回车

上述代码片断的前 2 行将 output 标记指向的数字 66 所在的内存地址送入 edx 寄存器,然后调用 addl 指令完成 66+20 的运算,addl 指令的目标操作数为 (%edx),而不是 %edx,用括号包围表示目标操作数在 %edx 指向的内存地址中。余下几行则通过将参数依次入栈,然后使用 UNIX 内核的系统调用从内核访问控制台显示,最后一行 int $0x80 表示使用 int 指令码,生成具有 0x80 值的软件中断,要求内核执行的具体操作由 eax 寄存器决定,这个内核函数省去了将每个输出字符亲自送到显示器的 I/O 地址的过程。该代码片断相当于执行以下 C 语句:

复制代码
output[0]+=20;
printf("%s",output);

第二块是退出程序,代码片断如下:

复制代码
pushl $0 # 参数一:退出代码
movl $1,%eax # 系统调用号(sys_exit)
int $0x80 # 调用内核功能

上述代码片断以退出代码 0 作为参数入栈,并以系统调用号 1 来调用内核,从而正常退出程序,这段代码相当于执行以下 C 语句:

复制代码
return 0;

以上两块代码均使用了 int 0x80访CPURing0int0x80 时,通过软中断触发内核事件,进而调用内核函数。调用函数通常需要传入参数,内核函数也不例外,栈就是用户程序与内核函数的交换空间。

与 Ubuntu 不同的是,FreeBSD 内核默认使用 C 语言的调用规范,因为作为一个类 Unix 操作系统,它遵守 Unix 规范,该规范允许任何语言所写的程序访问内核,也就是说 FreeBSD 访问内核的方式是先将参数压入栈中,然后再执行 int 0x8031pushint0x80 指令触发软件中断,执行内核函数,最后内核函数从栈中将参数取出,执行完毕后,由内核态返回用户态。

  1. Ubuntu 系统

Ubuntu 等 Linux 系统与 FreeBSD 等类 UNIX 系统在调用内核函数时略有不同。Linux 内核在传递参数的时候,使用了与 MS-DOS/Windows 相同的系统调用规范,例如,在 UNIX 的规范中,代表内核函数的数字存放在 eax 中,而在 Linux 中,调用参数并未压入栈中,而是存放在 ebx,ecx,edx,esi,edi,ebp 等寄存器中。因此在 Ubuntu 下,程序 3-1 需要稍做修改,修改后的代码如程序 3-2 所示:

程序 3-2 Ubuntu 下输出“B”
复制代码
.section .data
output:
.byte 66
.byte 10
.section .text # 代码段声明
.global _start # 指定入口函数
_start: # 在屏幕上显示一个字符串
movl $output,%edx
addl $20,%edx
movl $2, %edx # 参数三:字符串长度
movl $output, %ecx # 参数二:要显示的字符串
movl $1, %ebx # 参数一:文件描述符(stdout)
movl $4, %eax # 系统调用号(sys_write)
int $0x80 # 调用内核功能
# 退出程序
movl $0,%ebx # 参数一:退出代码
movl $1,%eax # 系统调用号(sys_exit)
int $0x80 # 调用内核功能

汇编并运行程序 3-2:

复制代码
$as -o 3-2.o 3-2.s
$ld -o 3-2 3-2.o
$ ./3-2
B

程序 3-2 与程序 3-1 的结构类似。首先,在可读写数据段(.section .data)中存放 66 以及 10(换行符的 ASCII 码);然后,在程序段(.section .text)的起始处,使用“.global _start ”声明入口程序名;最后,在 _start 程序中,先后使用两次“int $0x80”语句调用内核函数,显示字符串后退出程序。但有一个“陷阱”,程序段的前 2 行看似完成了 66+edx 的操作,但却忽略了一点,%edx 表示一个操作数,而不是操作数的内存位置。因此输出的是 66 对应的“B”,而不是 ASCII 码 86 对应的“√”。

程序 3-2 与程序 3-1 的主要区别在于:程序 3-2 并没有像程序 3-1 那样将调用参数推入栈中,而是将参数放在 edx、ecx、ebx、eax 寄存器中,然后调用内核功能输出字符串。

从以下代码片断可以看出:字符串长度 2 被放置在 edx 寄存器中,字符串的首地址放置在 ecx 寄存器中,输出设备 stdout 的描述符放置在 ebx 寄存器中,系统调用号 4 放置在 eax 寄存器中,最后一行调用了内核功能函数。

复制代码
movl $2, %edx # 参数三:字符串长度
movl $output, %ecx # 参数二:要显示的字符串
movl $1, %ebx # 参数一:文件描述符 (stdout)
movl $4, %eax # 系统调用号 (sys_write)
int $0x80 # 调用内核功能

C指针原理揭秘:基于底层实现机制(17):AT&T汇编概述 3.1.4

购书地址 https://item.jd.com/12533413.html?dist=jd

评论

发布