目录 Table of Contents
汇编编程示例-过程调用
幂计算 (power.s)
严格来讲汇编语言怎么传参数都可以, 但是为了方便, 我们还是选择用默认的 C 函数的传参规范来作为我们写汇编的规范.
.section .text
.globl _start
_start:
pushl $3 # push second argument
pushl $2 # push first argument
call power # call the function
addl $8, %esp # move the stack pointer back, 遵循 C 里面的规范, 调用者要自己恢复栈顶, 所以加 8
# 调用我们自己写的函数 power, 函数有两个参数分别压栈
pushl %eax # save the first answer, 遵循 C 规范, 返回值默认放到 eax 里面去, 我们把 eax 压栈, 这时候栈内存用于临时的存储空间
pushl $2 # push second argument
pushl $5 # push first argument
call power # call the function
addl $8, %esp # move the stack pointer back
popl %ebx # The second answer is already in %eax, we just pop the first out into %ebx.
#第二次调用的返回值已经在 eax 里面了, 我们这里把第一次调用的返回值取出来放到 ebx 里面
addl %eax, %ebx # 二者加一加, 结果放到 ebx 里面
movl $1, %eax # 系统调用 exit
int $0x80
.type powerm @function # 声明它是一个函数
power:
pushl %ebp # save old base pointer
movl %esp, %ebp # make stack pointer the base pointer
subl $4, %esp # get room for our local storage
movl 8(%ebp), %ebx # put first argument in %eax
movl 12(%ebp), %ecx # put second argument in %ecx
movl %ebx, -4(%ebp) # store current result
power_loop_start:
cmpl $1, %ecx # if the power is 1, we are done
je end_power
movl -4(%ebp), %eax # move the current result into %eax
imull %ebx, %eax # multiply the current result by the base number
movl %eax, -4(%ebp) # store the current power
decl %ecx # decrease the power
jmp power_loop_start # run for the next power
end_power:
movl -4(%ebp), %eax # return value goes in %eax
movl %ebp, %esp # restore the stack pointer
popl %ebp # restore the base pointer
ret
递归调用-阶乘 (factorial.s)
.section .text
.globl factorial # this is unneeded unless we want to share it
.globl _start
_start:
pushl $4 # The factorial takes one argument - the number we want a factorial of.
call factorial # run the factorial function
addl $4, %esp # restore the stack
movl %eax, %ebx # factorial returns the answer in %eax, but we want it in %ebx to send it as our exit status
movl $1, %eax # call the kernel's exit function
int $0x80
# This is the actual function definition
.type factorial, @function
factorial:
pushl %ebp
movl %esp, %ebp
movl 8(%ebp), %eax # This moves the first argument to %eax
cmpl $1, %eax # If the number is 1, that is our base case, and we simply return (1 is already in %eax as the return value)
je end_factorial
decl %eax # otherwise, decrease the value
pushl %eax # push it for our call to factorial
call factorial # call itself
movl 8(%ebp), %ebx # %eax has the return value, so we reload our parameter into %ebx
imull %ebx, %eax # multiply that by the result of the last call to factorial (in %eax); the answer is stored in %eax
end_factorial:
movl %ebp, %esp
popl %ebp
ret
文件处理
示例 1 - Uppercase
- 处理流程如下 :
-
打开输入文件
-
同时打开输出文件
-
如果输入文件读取位置已经到文件尾部, 则跳转到 step 7 程序结束
-
读取输入文件部分内容至内存
-
遍历该内容, 将其中的小写字母转换成大写
-
将转换后的该内容写入输出文件, 转到 step 3
-
程序结束
这里面我们会定义几个汇编里出现的一些符号, 有点像 C 里面的常量 #define
之类的.
.equ
用于把常量值设置为可以在程序中使用的 symbol.
例 : .equ factor, 3
把 factor 这个标号表示为 3; .equ LINUX_SYS_CALL, 0x80
我们要避免使用一些绝对的数据, 因为它的可读性不好嘛
当然了, 设置之后, 数据符号的值是不能在程序中改动的.
宏定义一些助记符 :
####### toupper.s #######
#system call numbers
.equ SYS_OPEN, 5
.equ SYS_WRITE, 4
.equ SYS_READ, 3
.equ SYS_CLOSE, 6
.equ SYS_EXIT, 1
#options for open
.equ O_RDONLY, 0
.equ O_CREAT_WRONLY_TRUNC, 03101
.equ O_PERMISSION, 0666
#standard file descriptors
.equ STDIN, 0
.equ STDOUT, 1
.equ STDERR, 2
#system call interrupt
.equ LINUX_SYSCALL, 0x80
.equ END_OF_FILE, 0 # This is the return value of read which means we've hit the end of the file
.section .bss
# Buffer - this is where the data is loaded into from the data file and written from into the output file.
# 我们这个例子就是把文件里面的一些数据取出来, 进行转换再写进去, 所以我们读文件的时候, 要读到内存里面的 buffer, 所以这里面我就声明了一个 buffer
.equ BUFFER_SIZE, 500 # buffer 长度为 500
# 把它声明到 .bss 里面, 因为这一段数据不需要初始化. 它在这里占了这么一个空间, 用来存放读取进来的数据, 它的地址是 buffer data
.lcomm BUFFER_DATA, BUFFER_SIZE # 同时它是 local
接下来我们看这个主程序, 看它做了什么
.section .text
#STACK POSITIONS
.equ ST_SIZE_RESERVE, 8
.equ ST_FD_IN, -4
.equ ST_FD_OUT, -8
.equ ST_ARGC, 0 # number of arguments
.equ ST_ARGV_0, 4 # number of program
.equ ST_ARGV_1, 8 # input file name
.equ ST_ARGV_2, 12 # output file name
.globl _start
_start:
movl %esp, %ebp
subl $ST_SIZE_RESERVE, %esp # allocate space for our file 给自己在栈里分配这么大的一块空间, 用来存放一些局部的一些变量
# 然后呢我们就 open file 了
open_files:
open_fd_in:
movl $SYS_OPEN, %eax # open syscall 系统调用
movl ST_ARGV_1(%ebp), %ebx # input filename into 这个实际上就是 ebp+8, 系统调用通过栈来传递参数, ebp+0 就是你带多少个参数, ebp+4 就是告诉你你这个程序本身的名字, ebp+8 +12 就是命令行程序后面跟的一堆参数, 这里的参数就是我们要打开的文件
movl $O_RDONLY, %ecx
movl $O_PERMISSION, %edx # 这里是完全按照 open 系统调用的规范, 挨个往里面填参数
int $LINUX_SYSCALL # 调用系统调用
store_fd_in:
movl %eax, ST_FD_IN(%ebp) # save the given file descriptor 我们把这个输入文件打开之后, 把它的文件描述符存到 ST_FD_IN - 4 也就是 ebp - 4 这个地方. 刚才我们在程序栈空间里面开了一个临时存储区域, ebp - 4 就是我们存输入文件描述符的位置.
open_fd_out:
movl $SYS_OPEN, %eax # open the file
movl ST_ARGV_2(%ebp), %ebx
movl $O_CREAT_WRONLY_TRUNC, %ecx # flags for writing to the file mode for new file, 打开一个可写的文件, 如果这个文件当前存在的话, 就把它清空
movl $O_PERMISSION, %eax
int $LINUX_SYSCALL
store_fd_out:
# store the file descriptor here, 打开之后, 我就把这个打开的文件的描述符, 还是存到我这个程序当前的栈空间里面去, 刚才是 ebp - 4, 所以这里应该是 ebp - 8
movl %eax, ST_FD_OUT(%ebp)
然后我们开始主循环
主循环不断从源文件里面读取一个定长的数据, 实际上就是刚才 500 字节的 buffer. 然后如果没有到文件末尾的话, 我就把读取进来的 500 字节的 buffer 扫一遍, 发现如果有小写字符, 就转换成大写, 然后把它放到我们程序的输出文件里面去
###BEGIN MAIN LOOP###
read_loop_begin:
movl $SYS_READ, %eax
movl ST_FD_IN(%ebp), %ebx # get the input file descriptor
movl $BUFFER_DATA, %ecx # the location to read into
movl $BUFFER_SIZE, %edx # the size of the buffer
int $LINUX_SYSCALL # size of the buffer read is returned in %eax
###EXIT IF WE'VE REACHED THE END###
cmpl $END_OF_FILE, %eax # check for end of file marker
jle end_loop # if found or on error, go to the end, 0 的话就是文件末尾了, 小于 0 的话就是出错了
continue_read_loop:
###CONVERT THE BLOCK TO UPPER CASE###
pushl $BUFFER_DATA # location of buffer
pushl %eax # size of the buffer
call convert_to_upper # 调用函数, 一个参数是你要转换的字符串的起始地址, 另一个参数就是你字符串的长度
# eax 就是刚才系统调用的返回值, 如果返回值正确的话, 就存放你实际读取的数据的长度
popl %eax # get the size back, 把读出来的 read buffer 长度, 到底有效数据是多少, 给它恢复一下
addl $4, %esp # restore %esp
###WRITE THE BLOCK OUT TO THE OUTPUT FILE###
movl %eax, %edx # size of the buffer
movl $SYS_WRITE, %eax
movl ST_FD_OUT(%ebp), %ebx # the output file descripter
movl $BUFFER_DATA, %ecx # location of the buffer
int $LINUX_SYSCALL
###CONTINUE THE LOOP###
jmp read_loop_begin
# 进入新一轮循环, 先打开源文件, 从第一个文件开始读 500 个, 判断有没有到末尾, 没有到末尾的话, 看一看它的长度是怎么样子, 完了就是挨个扫一遍, 把小写字母转换成大写字母, 把它写出去, 周而复始, 一直到程序末尾为止.
end_loop:
# 因为我们两个文件都打开了, 所以我们要在程序退出之前, 也就是刚才循环做完的时候, 把这两个文件关掉
###CLOSE THE FILES###
movl $SYS_CLOSE, %eax
movl ST_FD_OUT(%ebp), %ebx
int $LINUX_SYSCALL
movl $SYS_CLOSE, %eax
movl ST_FD_IN(%ebp), %ebx
int $LINUX_SYSCALL
###EXIT###
movl $SYS_EXIT, %eax
movl $0, %ebx
int $LINUX_SYSCALL # 通过系统调用来退出
接下来我们来讲一讲刚才那个没有说的 function, 就是把一段 buffer 里面的小写字母转换成大写字母的这么一个函数.
这个函数有两个参数, 一个是你这个数据的地址, 另一个是数据有多长
函数的原理是把输进来的字符挨个扫一遍, 看看每一个是不是位于 a 和 z 之间, 包括 a 和 z. 如果在这之间呢就把它加上这么一个 A
- a
大 A 减去小 a 的这么一个 offset 偏移量, 就是 ascii 码加上一个差值, 这样就能完成一个转换了.
#conver_to_upper function
#INPUT : The first parameter is the location of the block of memory to convert.
# The second parameter is the length of that buffer
#OUTPUT : This function overwrites the current buffer with the new version.
###CONSTANTS###
.equ LOWERCASE_A, 'a' # The lower boundary of our search
.equ LOWERCASE_Z, 'z' # The upper boundary of our search
.equ UPPER_CONVERSION, 'A' - 'a'
###STACK STUFF###
.equ ST_BUFFER_LEN, 8 # Length of buffer
.equ ST_BUFFER, 12 # actual buffer
convert_to_upper:
pushl %ebp
movl %esp, %ebp
###SET UP VARIABLES###
movl ST_BUFFER(%ebp), %eax
movl ST_BUFFER_LEN(%ebp), %ebx
movl $0, %edi # Loop variable, 这边就给出一个循环变量, 循环变量也容易了, 我们从 eax 这个地址出发, 往里面挨个的扫, 一个一个找, 找 ebx 的变, 找到一个小写的就把它转换成大写的
#if a buffer with zero length was given to us, just leave
cmpl $0, %ebx
je end_convert_loop
conver_loop:
movb (%eax, %edi, 1), %cl # get the current byte, 获得当前的这个 byte, eax 是起始地址, 加上 index 乘上 1 了
cmpb $LOWERCASE_A, %cl # go to the next byte unless it is between 'a' and 'z'
jl next_byte
cmpb $LOWERCASE_Z, %cl
jg next_byte
# otherwise convert the byte to uppercase, and store it back
addb $UPPER_CONVERSION, %cl
movb %cl, (%eax, %edi, 1)
next_byte:
incl %edi # next byte
cmpl %edi, %ebx # continue unless we've reached the end, ebx 存放的是你当前有多少个字符需要处理, 二者对比, 如果相等的话, 就退出了, 如果不相等就继续回去
end_convert_loop:
movl %ebp, %esp
popl %ebp
ret
示例 2 - 数据记录处理
程序 1 - 写数据记录文件
尝试多个 .s 模块形成一个可执行文件
- 程序流程 :
-
Open the file
-
Write three records
-
Close the file
#linux.s
#Common Linux Definitions
#System Call Numbers
.equ SYS_EXIT, 1
.equ SYS_READ, 3
.equ SYS_WRITE, 4
.equ SYS_OPEN, 5
.equ SYS_CLOSE, 6
.equ SYS_BRK, 45
#System Call Interrupt Number
.equ LINUX_SYSCALL, 0x80
#Standard File Descriptors
.equ STDIN, 0
.equ STDOUT, 1
.equ STDERR, 2
#Common Status Codes
.equ END_OF_FILE, 0
#record-def.s
#结构体 struct, 下面的都是连续存放的
.equ RECORD_FIRSTNAME, 0
.equ LASTNAME, 40
.equ RECORD_ADDRESS, 80
.equ RECORD_AGE, 320
.equ RECORD_SIZE, 324
我们定义了一些助记符常量, 使用的时候可以用 .include "linux.s"
, .include "record-def.s
, 和 C 里面头文件一样
#read-record.s
.include "record.s"
.include "linux.s"
#INPUT : The file descriptor and a buffer
#OUTPUT : This function writes the data to the buffer and returns a status code
#stack procedural parameters
.equ ST_READ_BUFFER, 8
.equ ST_FILEDES, 12
.section .text
.globl read_record # 因为这个函数要被别的 .s 调用, 所以它要全局可见
.type read_record, @function
read_record:
pushl %ebp
movl %esp, %ebp
pushl %ebx
movl ST_FILEDES(%ebp), %ebx
movl ST_READ_BUFFER(%ebp), %ecx
movl $RECORD_SIZE, %ebx
movl $SYS_READ, %eax
int $LINUX_SYSCALL
#NOTE : %eax has the return value
popl %ebx
movl %ebp, %esp
popl %ebp
ret
#write-record.s
.include "linux.s"
.include "record-def.s"
.section .data
record1:
.ascii "Fredrick\0"
.rept 31 # Padding to 40 bytes.
.byte 0 # 就是把 byte 0 重复 31 次, 把这个空间填充到 40 字节
.endr
.ascii "Bartlett\0"
.rept 31 # Padding to 40 bytes.
.byte 0
.endr
.ascii "4242 S Prairie\nTulsa, OK 55555\0"
.rept 209 # Padding to 240 bytes. 他的地址, 充满 240 字节.
.byte 0
.endr
.long 45
# .rept N 表示汇编器重复填充与 .endr 之间的内容, 重复 N 次
# 我们就用这种比较笨的方式, 直接把这里面几个record 写到程序里面, 写死了
#record2: #skip record2
#......
#This is the name of the file we will write to
file_name:
.ascii "test.dat\0" # 我们输出的文件名字就是它, 它没有指明任何一个绝对的路径, 那应该就在当前的路径下.
.equ ST_FILE_DESCRIPTOR, -4
.globl _start
_start:
movl %esp, %ebp
subl $4, %esp # 分配 4 字节的局部空间
movl $SYS_OPEN, %eax
movl $file_name, %ebx
movl $0101, %ecx # 打开一个文件, 如果文件不存在, 就创建一个, 就是为了写操作, 然后给它指明一些访问权限, 最终返回值放到 eax 里面去
movl $0666, %edx # 指明权限
int $LINUX_SYSCALL
movl %eax, ST_FILE_DESCRIPTOR(%ebp) # 文件描述符返回值放到 eax 里面, 这里把文件描述符存到 ST_FILE_DESCRIPTOR(%ebp), 就是当前函数里的局部空间
pushl ST_FILE_DESCRIPTOR(%ebp) # Write the first record, 怎么写文件呢 ? 首先 push 你要写的那个目标文件
pushl $record1 # 然后 push record1 也就是你要写的那个数据的存放地址, 就是 record1. 注意 record1 本身是个地址, 这个地址作为常量来传的, 所以它前面要加 $.
call write_record
addl $8, %esp
# Write the remaining records, close the file and exit !
# 我们这个程序是 demo 只写一个, 你想要写多少个在这里重复添加就好.
最后使用汇编命令 as write-record.s -o write-records.o
as write-record.s -o write-record.o
ld write-record.o write-records.o -o write-records
连接在一起, 这个程序由两个 .s 构成
程序 2 - 读数据处理文件
- 程序要求 :
打开一个输入文件, 把里面 record 一个读取出来, 把里面的 first name 输出, 输出到标准输出里面去, 输出到命令行上面去.
- 程序流程 :
-
Open the file
-
Attempt to read a record
-
If we are at the end of file, exit
-
Otherwise, count the characters of the first name, 统计一下 first name 的长度
-
Write the first name to STDOUT, 把名字写到标准输出
-
Write a newline to STDOUT, 换行
-
Go back to read another record, 继续循环, 直到把所有记录读完
- 额外实现了两个函数 :
显示一个新行 ;
计算字符串长度 (类似 strlen) .
- 具体程序如下 :
统计字符串长度函数 :
#count-chars.s :
#INPUT : The address of the chars.
#OUTPUT : Returns the count in %eax
.type count_chars, @function
.globl count_chars
.equ ST_STRING_START_ADDRESS, 8 # stack procedural parameter
count_chars:
pushl %ebp
movl %esp, %ebp
movl $0, %ecx # Counter starts at zero
movl ST_STRING_START_ADDRESS(%ebp), %edx # Starting address of data
count_loop_begin:
movb (%edx), %al # Grab the current character
cmpl $0, %al # Is it null ? 如果 char 是空的话, 就说明已经到了字符串的末尾, 这种情况下就退出了.
je count_loop_end
incl %ecx # ecx 是所统计的字符串的个数
incl %edx # edx 相当于就是这个下标 index
jmp count_loop_begin
count_loop_end:
movl %ecx, %eax # move the count into %eax
popl %ebp
ret
换行函数 :
#write-newline.x
.include "linux.s"
.section .data
newline:
.ascii "\n"
.section .text
.equ ST_FILEDES, 8
.globl write_newline
.type write_newline, @function
write_newline:
pushl %ebp
movl %esp, %ebp
movl $SYS_WRITE, %eax
movl ST_FILEDES(%ebp), %ebx # file descriptor
movl $newline, %ecx
movl $1, %edx
int $LINUX_SYSCALL
movl %ebp, %esp
popl %ebp
ret
# 这第二个函数就简单了, 就是输出一个换行符. 我们把它输出在标准输出里面.
主程序 :
# read-records.s the main program.
.include "linux.s"
.include "record-def.s"
.section .data
file_name:
.ascii "test.dat\0"
.section .bss
.lcomm record_buffer, RECORD_SIZE # 文件读取进来然后放到这里面去
.equ ST_INPUT_DESCRIPTOR, -4 # These are the locations on the stack where we will store the descriptors.
.equ ST_OUT_DESCRIPTOR, -8 # 输入输出文件的描述符, 都存放在我的局部的栈里面
.section .text
.globl _start
_start:
movl %esp, %ebp
subl $8, %esp # Allocate space to hold the descriptors. -8 就是我要存储两个描述符, 在栈里开一点空间
movl $SYS_OPEN, %eax
movl $file_name,%ebx
movl $0, %ecx # This says to open read-only
movl $0666, %edx
int $LINUX_SYSCALL # Open the file
movl %eax, ST_INPUT_DESCRIPTOR(%ebp) # Save input file descriptor.
movl $STDOUT, ST_OUTPUT_DESCRIPTOR(%ebp)
record_read_loop:
pushl ST_INPUT_DESCRIPTOR(%ebp)
pushl $record_buffer
call read_record # Get one record
addl $8, %esp
cmpl $RECORD_SIZE, %eax # 把我们返回值 eax 与文件大小比较一下, 如果不相等就说明文件已经读到末尾了.
jne finished_reading # Otherwise, print out the first name; but first, we must know it's size
# 我们是通过系统调用 write 来输出的, 所以我们需要事先知道它的这个 size. 在这种情况下我们 call 一个 count buffer chars, 调用一下这个函数来统计我们当前读出这个 first name 长度为多少.
pushl $RECORD_FIRSTNAME + record_buffer # 注意这里面压了一个参数, 这个参数 record_buffer 是一个地址, 这个地址可以理解为一个常量, 这个常量加上一个 record_firstname, 就相当于是它的一个偏移量. 刚才说过, record 里面第一个就是 firstname offset, 当然它应该是 0, 这样相加之后我们就知道了这个 first name 地址是多少, 这个地址作为一个常量压栈.
call count_chars # 调用计数函数
addl $4, %esp
movl %eax, %edx # 把返回值 eax 放到 edx 里面去, 我们要进行系统调用, 所以要把 eax 腾出来
movl ST_OUTPUT_DESCRIPTOR(%ebp), %ebx
movl $SYS_WRITE, %eax
movl $RECORD_FIRSTNAME + record_buffer, %ecx
int $LINUX_SYSCALL # Print the first name
# 打一个换行符出来, 让最终结果更加美观
pushl ST_OUTPUT_DESCRIPTOR(%ebp)
call write_newline
addl $4, %esp
jmp record_read_loop
finished_reading:
... # exit the program
程序 3 - 修改数据记录
-
Opens an input and output file
-
Reads records from the input
-
Increments the age
-
Writes the new record to the output file
这个留作练习