汇编语言程序设计读书笔记(3)- 程序范例

主要描述三方面的内容:第一是汇编语言的程序模版,以及模版涉及到的一些知识点;第二是如何调试汇编语言;第三是如何在汇编语言中调用C库函数。

1. 汇编语言的组成

汇编语言由段(section)组成,一个程序中执行的代码,叫文本段(text),程序还可能有定义变量,有付给初始值的变量放在数据段(data)中,没有赋初值或者付给零初值的放在bss段中。text段一定是要有的,data和bss可以没有。

2. 段的定义

用.section语法定义段。比如:

.section .text定义文本段,

.section .data定义数据段,

.section .bss定义bss段。

顺序没有必须的要求,但为了便于别人接手和理解你的程序,书中建议采用从上到下按照data,bss,text段的顺序定义。

3. 定义程序起始点

文本段必须要定义一个程序执行的起始点,ld默认为_start;而gcc默认会连接标准的库代码,该代码入口为_start,执行一段程序后跳到main执行,因此gcc默认要求外部源程序定义main,且不能定义_start,但如果使用参数-nostdlib,那么就不会默认连接标准的库代码,此时入口点用_start也没问题;此外,ld和gcc都支持-e参数来指定入口点,此时任意的标号都可以用作入口点。

下面以例子来说明。以汇编语言程序设计读书笔记(2)- 相关工具64位系统篇中的cpuid2.s为例子进行说明,目前可能读者还不理解该程序的细节,但本文后面会论述,读完本文后,理解该程序不是问题。目前只要清楚该程序用于输出CPU的ID厂商的字符串。源程序入口为_start。如下:

cpuid2.s# cpuid2.s file
.section .data
output:
    .asciz "CPUID is ‘%s‘\n"
.section .bss
    .lcomm buffer, 12
.section .text
.globl _start
_start:
    nop
    movl $0, %eax
    cpuid

    movl $buffer, %edi
    movl %ebx, (%edi)
    movl %edx, 4(%edi)
    movl %ecx, 8(%edi)

    pushl $buffer
    pushl $output
    call printf

    addl $8, %esp

    pushl $0
    call exit

下面分别对入口为_start,main,xxxx(任意标签)这三种情况下,用as,ld和gcc汇编cpuid2.s,生成可执行文件。

1). 入口为_start

用as,ld生成可执行文件(入口为_start),如下图所示:

gcc生成可执行文件(入口为_start),如下图:

2). 入口为main

源程序把标签_start改为main。此时ld可以使用-e main参数,那么就会以main为程序起始点;gcc则有两种方式,一是使用默认的编译方式,即不带-nostdlib参数,此时gcc会连接库代码,所以生成的执行文件的大小较大,另一种方式是使用-nostdlib,但是用-e main指定入口点。

用as,ld生成可执行文件(入口为main),如下图所示:

gcc按第一种方式生成可执行文件(入口为main),如下图:

gcc按第二种方式生成可执行文件(入口为main),如下图:

第一种方法生成的大小为4799,第二生成的大小为2133。

3). 入口为xxxx(任意的标号)

cpuid2.s源程序把_start改为xxxx,即入口为xxxx。此时必须使用-e xxxx来汇编或者编译。

用as,ld生成可执行文件(入口为xxxx),如下图所示:

gcc生成可执行文件(入口为xxxx),如下图:

可见入口为xxxx的方式包含了以上入口为_start和main的情况。

综合以上情况,无论入口点是什么标签,无论是运行在32位系统或者64位系统,都可以按照以下的命令来汇编和编译汇编程序。

# 假设要汇编或编译的汇编程序有n个,为input_file1.s,input_file2.s,...,_filen.s。n大于等于1。
# 输入文件列表input_file1.s,...input_filen.s使用{input_file.s}表示,同样,{input_file.o}表示一系列的.o文件
# 输出可执行文件为output_file。
# libc.so所在的绝对路径用/libc_path表示,ld-linux.so.2所在的绝对路径用/ld-linux_path表示。
# 入口点标签为entry_point。
# []括起来的是调用了C库函数才需要的部分,去过没有调用C库,则不需要。
# 那么as,ld生成可执行文件的命令如下:
as --32 -o input_file1.o input_file1.s
as --32 -o input_file2.o input_file2.s
...
as --32 -o input_filen.o input_filen.s

ld -m elf_i386 -e entry_point [-dynamic-linker /ld-linux_path/ld-linux.so.2] -o output_file [-L/libc_path -lc] {input_file.o}

# gcc生成可执行文件的命令如下:

gcc -m32 -e entry_point -nostdlib -o output_file [-L/libc_path -lc] {input_file.s}


#注:如果源程序没有调用C库函数而又使用了[]中的指令连接或编译,那么会产生错误“/usr/lib/libc.so.1: bad ELF interpreter: 没有那个文件或目录”

4. 外部程序标签声明

如果一个汇编程序文件中的代码调用了另一个汇编程序文件的标号或者函数,那么必须声明这个标号或函数为.globl(应该是global的缩写,全局的,可以跨文件调用)的。

比如汇编程序test3.s和test4.s,test3.s调用了test4.s的函数fun4,如果test4.s没有.globl fun4这行,那么编译会提示错误,加上这行则没有任何问题。

test3.s.section .text
.globl _start
_start:
	call fun4
	movl $1, %eax
	movl $0, %ebx
	int $0x80

test4.s.section .text
#.globl fun4
fun4:
	movl $1, %eax
	movl $2, %ebx
	addl %ebx, %eax
	ret

#符号把.globl fun4注释了,那么汇编成test3.o和test4.o后连接,会提示在函数_start中,没有定义fun4。如下图:

把#去掉后,让.globl fun4起作用,则没有任何问题,如下图:

综上,任何的标号或者函数,如果要准备给别的文件调用,那么一定要用.globl声明。

5. 汇编程序模版

经过上面的论述后,很容易得到了汇编程序的模版,如下:

.section .data
    <初始化值的数据在这里>
.section .bss
    <未初始化的数据在这里>
.section .text
.globl entry_point
entry_point:
    <代码指令在这里>

其中,entry_point为程序起始点。

6. 汇编程序范例

书中的范例是用CPUID汇编指令去读取CPU的厂商ID(Vendor ID)。了解这个程序之前先简单说明几个知识点。

1). 关于CPUID指令

输入参数通过寄存器EAX传入,执行CPUID后,输出通过EBX,ECX,EDX传出。这里只要了解EAX=0时,ECX,EDX,EBX分别得到厂商ID的字符串的高4字节,中间4字节,低4字节。厂商ID的字符串按照小端排列,即先放低字节,即厂商ID为[EBX][EDX][ECX]。

这可以通过对CPUID的测试代码test_cpuid.s来进一步了解,如下代码:

test_cpuid.s# test_cpuid.s program
.section .text
.globl _start
_start:
	nop
	movl $0, %eax
	CPUID
	movl $1, %eax
	movl $0, %ebx
	int $0x80

带调试参数-gstabs生成执行文件后用kdbg调试,在CPUID指令后设置断点,如下图:

寄存器ebx,edx,ecx,每个字节都是ASCII的编码,把这些字节编码按照ebx,edx,ecx从低字节到高字节的排列翻译成字符串,如下图,显示如下:

即厂商ID是“GenuineIntel”。

2). 关于Linux系统调用

通过0x80号的软件中断(int $0x80),可以调用Linux的内核函数,具体是哪个内核函数,由EAX寄存器决定,而传递给函数的参数则根据调用的函数而有不同的含义,一般由EBX,ECX,EDX来传递。书籍要到第十二章才会进一步讨论。目前使用到的两个系统调用先要简单了解。

第一个是第1号调用,调用的是退出函数sys_exit(ret),EAX=1表示调用号,EBX=ret传递第一个参数,表示返回给父进程的返回值,即sys_exit(ret)相当于如下的汇编代码:

# sys_exit(ret)系统调用的汇编代码
movl $1, %eax
movl $ret, %ebx
int $0x80

第二个调用是第4号调用,调用的是函数sys_write(int fd, const void *buf, size_t count),三个参数分别用ebx,ecx,edx传入,分别代表文件描述符,要写的缓冲区首地址,缓冲区字节长度。众所周知,Linux中用文件描述符1用于表示标准输出(stdout),默认即显示终端,因此要往显示终端打印长度为length的字符串str,即sys_write(1, str, length)相当于以下的汇编代码:

# sys_write(1, str, length)系统调用的汇编代码
movl $4, %eax
movl $1, %ebx
movl $str, %ecx
movl $length, %edx
int $0x80

3). 完整的代码范例cpuid.s

如下的代码cpuid.s,读取CPU的厂商ID,然后打印到屏幕上。代码为:

cpuid.s# cpuid.s程序,打印CPU的厂商ID
.section .data
output:
    .ascii "CPU ID is ‘xxxxxxxxxxxx‘\n"    #在data段定义字符串,最后的结果取代xxxxxxxxxxxx后就是要输出的内容
.section .text
.globl _start
_start:
    movl $0, %eax                                  #获取CPU厂商ID到ebx,edx,ecx
    cpuid

    movl $output, %edi
    movl %ebx, 11(%edi)
    movl %edx, 15(%edi)
    movl %ecx, 19(%edi)                      #更新xxxxxxxxxxxx,在字符串中是第11个字节开始

    movl $4, %eax
    movl $1, %ebx
    movl $output, %ecx
    movl $25, %edx
    int $0x80                                            #输出字符串,包括换行在内长度为25字节

    movl $1, %eax
    movl $0, %ebx
    int $0x80                                           #退出程序,返回0值

生成可执行文件以及执行的结果如下图:

理解了上述的1)和2)所述的知识,加上代码中的注释,这段代码不难理解,前提是需要有一点点汇编语言的基础(不会movl都不知道吧?),不再赘述。

7. 调试汇编程序

用kdbg调试程序可以不用记gdb的指令,kdbg整个操作界面十分明朗,要单步运行,设置断点,观察变量,寄存器,内存,都没有问题,如下图:

但有一个特别需要注意的地方,_start之后的第一个nop指令,如果没有,没法观察内存,会提示超出范围,后来把nop加上则一切正常,该书上提到没有这个nop会造成无法在_start处设置断点,为gdb的bug,而我在kdbg中则是无法擦看内存,断点倒是可以设置。所以还是建议调试时,先增加这个nop指令。

8. 调用C库函数

上面的cpuid.s代码范例使用软中断调用linux内核函数来实现打印和退出程序,还有另外的方法实现打印和退出,那就是调用C语言的标准库函数,打印是printf,退出是exit。

假如CPUID=”GenuineIntel”,那么可以使用printf(“CPU ID is ‘%s’\n”,CPUID)来打印和cpuid.s一样的输出信息。下面就以printf为例来描述汇编程序怎么调用C库函数。

调用子程序的指令是call,那么调用printf就是用call printf,如果要实现printf(“CPU ID is ‘%s’\n”,CPUID),那里面的两个参数怎么传递?这个要使用堆栈(stack),一般而言,C语言采用从右到左的顺序入栈,即字符串CPUID的首地址先入栈,而后“CPU ID is ‘%s’\n”的首地址入栈。入栈的汇编代码是pushl。因此,可以按照如下的代码实现printf(“CPU ID is ‘%s’\n”,CPUID)。

# 假设"CPU ID is ‘%s‘\n"的首地址是output,读回来的CPUID字符串首地址为buffer
# 那么以下代码实现C库函数调用:printf(output, buffer)
pushl $buffer
pushl $output
call printf

注意,C语言堆栈底部用高地址,pushl入栈后堆栈指针esp寄存器变小了4,因此,如果在call printf后,buffer和output不再使用了,可以把esp设置为指向入栈前的地址,以便这两个参数占用的堆栈空间可以使用。即pushl两次后,esp减少了8,因此esp需要加上8才能回复原来的堆栈位置,即addl $8, %esp。

这里可以简单的理解:对于C库函数func(param1, param2, …, paramn),调用的方法是先参数从右到左的顺序入栈,然后call func。对于要深入研究的话,提供abi接口文档下载

exit(ret)的调用方式轻易实现:

# exit(ret)的汇编调用代码
pushl $ret
call exit

32位系统的abi和64位的abi(application binary interface)是不一样的,因此x64的系统不能如此调用,所以在64位系统上运行32位的程序,必须按照32位的汇编和连接,否则会发生错误。具体可参考汇编语言程序设计读书笔记(2)- 相关工具64位系统篇一文。

9. 通过调用C库函数改写cpuid.s-cpuid2.s范例

其实这个范例就是“3. 定义程序起始点”内容中的范例。理解了cpuid.s的意思,理解了怎么调用C库函数,那么这个范例就可以轻易的理解了。另外对于这个范例还需说明两点。

.asiz: 因为printf打印的是0结尾的字符串,因此需要定义0结尾的字符串,所以用.asciz,而不用.ascii。

.lcomm: 声明留出一块本地内存,这里.lcomm buffer, 12表示留出12个字节大小的一块本地内存,首地址用buffer表示。

代码很明了,先是读取CPUID到ebx,edx,ecx寄存器,然后通过edi寄存器为索引,把读到的的字符串拼接到buffer中,然后分别以output和buffer为参数调用printf,相当于调用了printf(“CPU ID is ‘%s’\n”, buffer)来打印,最后调用exit(0)返回。

10. 结束语

通过本篇的描述,应该知道怎样设计一个汇编程序,包括系统调用和C库调用怎样使用,最后在32位系统或64位系统上汇编连接运行。

时间: 2024-10-13 13:27:38

汇编语言程序设计读书笔记(3)- 程序范例的相关文章

汇编语言程序设计读书笔记(2)- 相关工具64位系统篇

汇编语言程序设计一书,在32位系统下应该不会有什么问题,然而在64位系统下,则会有些不一样的地方.有些程序范例还会汇编错误或者执行错误. 博主所用系统为CentOS v6.4 x64.本文主要解决32位的汇编程序如何在64位环境下汇编.连接,而不论述64位汇编语言如何设计. 1. 64位系统下编译32位的C程序 以程序test5.c为例,程序代码很简单,如下: test5.c#include <stdio.h> int main() { char str[4]; str[0]='f'; str

汇编语言程序设计读书笔记(1)- 相关工具

linux下汇编语言采用的是AT&T语法,可使用GNU工具,包括汇编器gas,连接器ld,编译器gcc,调试器gdb或kdbg,objdump的反汇编功能,简档器gprof.以简单的例子分别对每个工具在汇编语言开发中的用法进行简单说明. 这些工具都要在linux环境下使用,先建立linux的开发环境,可参考文章“windows7 64位系统安装VMware Centos 64位系统搭建开发环境”. 假设有以下简单的c程序test1.c. #include <stdio.h> int m

汇编语言程序设计读书笔记(4)- 程序设计基础之一

目录: 一.数据定义 1.变量数据定义 2.常量数据定义 3.缓冲区定义 二.寻址方式 1.立即数寻址 2.寄存器寻址 3.直接寻址 4.寄存器间接寻址 5.寄存器相对寻址 6.变址寻址 三.数据传送和mov指令 1.数据传送规则 2.mov指令 四.条件传送数据cmov指令 1.状态标志位 2.cmov指令 五.交换数据 1.xchg指令 2.bswap指令 3.xadd指令 4.cmpxchg指令 5.cmpxchg8b指令 六.堆栈 1.堆栈简介 2.入栈指令push 3.出栈指令pop

读书笔记2013-4 程序员的思维修炼

  程序员的思维修炼:开发认知潜能的九堂课 (图灵程序设计丛书) (亨特(Andy Hunt))> 简介 本书解释了为什么软件开发是一种精神活动,思考如何解决问题,并就开发人员如何能更好地开发软件进行了评论.书中不仅给出了一些理论上的答案,同时提供了大量实践技术和窍门. 本书供各层次软件开发人员阅读. 1. 软件是在头脑中创建的 思维和概念是需要在团队(也包括付钱让我们开发软件的人)中分享和交流的.现在我们需要研究的真正难题是团队内部和团队间的交流,甚至更困难的问题是完全陈旧的思想.没有任何项目

读书笔记:程序员的自我修养-----第一章(综述)

题前:30--45天读完,一周至少3篇读书笔记.不能坚持,不再联系,不再找你. 一. hello world 程序引出的问题,看40天后,再回来看看自己的答案,提升多少. Q1:程序为什么要被编译器编译之后才可以运行?   A1 : 系统执行的机器语言,即二进制文件,程序是文本文件需要编译之后,由链接器链接需要的基本库生成二进制文件. Q2: 编译器在把C语言程序转换成可以执行的机器码的过程中作了什么,怎么做的?   A2: 预处理,汇编器生成汇编文件,编译器生成目标文件,链接器链接生成可执行文

【读书笔记】程序员的自我修养总结(六)

[读书笔记]程序员的自我修养总结(六) 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 说明:这是程序员的自我修养一书的读书总结,随着阅读的推进,逐步增加内容. 本文主要介绍可执行文件的装载与进程 程序与进程的区别 程序是静态的,指的是一些预先编译好的指令和数据集合的一个文件:而进程实际上就是运行着的程序,是动态的. 虚拟地址空间 程序运行起来后将拥有独立的虚拟地址空间 virtual address space,其大小由计算机的硬件平台决定,具体地说是

读书笔记:程序员的自我修养-----第三章(目标文件)

一 .目标文件格式 1. PE(Portabel Executable) 2. ELF (Executable Linkable Format) : 可重定位.可执行.共享目标文件.核心转储文件 思考: 弱符号和弱引用   VS   回调函数 __attrbute__ ((weakref)) void foo( ); int main() { if( foo ) foo(); } 可以被用户的强符号覆盖: 读书笔记:程序员的自我修养-----第三章(目标文件)

【读书笔记】程序员的自我修养总结(二)

程序员的自我修养总结(二) 声明:引用请注明出处http://blog.csdn.net/lg1259156776/ 说明:这是程序员的自我修养一书的读书总结,随着阅读的推进,逐步增加内容. 由源文件到可执行文件 分为四个步骤: 预处理 处理源代码中以#开始的预编译指令,进行宏定义展开,处理所有条件预编译指令,将被包含文件插入到预编译指令的位置,删除所有注释,添加行号及文件标识,保留#pragma编译器指令,因为编译器需用到. 编译 进行一系列词法分析,语法分析,语义分析及优化后生成汇编代码文件

JavaScript高级程序设计-读书笔记(2)

第6章 面向对象的程序设计 创建对象 1.最简单方式创建Object的实例,如 var person = new Object(); person.name = “Greg”; person.age = 27; person.job = ”Doctor”; person.sayName = function() { alert(this.name); }; person. sayName(): 缺点:会产生大量重复代码 2.工厂模式:用函数来封装以特定接口创建对象的细节,如 function c