5_汇编编程示例-过程调用

汇编编程示例-过程调用

幂计算 (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

  • 处理流程如下 :
  1. 打开输入文件

  2. 同时打开输出文件

  3. 如果输入文件读取位置已经到文件尾部, 则跳转到 step 7 程序结束

  4. 读取输入文件部分内容至内存

  5. 遍历该内容, 将其中的小写字母转换成大写

  6. 将转换后的该内容写入输出文件, 转到 step 3

  7. 程序结束

这里面我们会定义几个汇编里出现的一些符号, 有点像 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 模块形成一个可执行文件

  • 程序流程 :
  1. Open the file

  2. Write three records

  3. 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 输出, 输出到标准输出里面去, 输出到命令行上面去.

  • 程序流程 :
  1. Open the file

  2. Attempt to read a record

  3. If we are at the end of file, exit

  4. Otherwise, count the characters of the first name, 统计一下 first name 的长度

  5. Write the first name to STDOUT, 把名字写到标准输出

  6. Write a newline to STDOUT, 换行

  7. 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 - 修改数据记录

  1. Opens an input and output file

  2. Reads records from the input

  3. Increments the age

  4. Writes the new record to the output file

这个留作练习