32位汇编语言学习笔记(45)--测试简单文件操作接口(完)

时间:2022-05-26 01:18:13


这是《Assembly Language step by step programming with linux》书中的最后一个程序,也是全书中的最复杂的一个程序。
首先看一下这个程序使用的一些新的c接口:
FILE *fopen( const char *filename, const char *mode );
int fclose( FILE *stream );
char *fgets( char *string, int n, FILE *stream );
int fprintf( FILE *stream, const char *format [, argument ]...);
int sscanf( const char *buffer, const char *format [, argument ] ... );

此程序分成两个文件:linlib.asm和textfile.asm。linlib.asm实现几个公共函数,主程序在textfile.asm中。
linlib.asm文件内容如下:

[SECTION .data]		; Section containing initialised data

[SECTION .bss] ; Section containing uninitialized data

[SECTION .text] ; Section containing code

extern printf ; All of these are in the standard C library glibc
extern rand
extern srand
extern time

global seedit ; Seeds the random number generator with a time value
global pull31 ; Pulls a 31-bit random number
global pull16 ; Pulls a 16-bit random number; in the range 0-65,535
global pull8 ; Pulls an 8-bit random number; in the range 0-255
global pull7 ; Pulls a 7-bit random number; in the range 0-127
global pull6 ; Pulls a 6-bit random number; in the range 0-63
global pull4 ; Pulls a (marginal) 4-bit random number; range 0-15
global newline ; Outputs a specified number of newlines to stdout

pull31: mov ecx,0 ; For 31 bit random, we don't shift
jmp pull
pull16: mov ecx,15 ; For 16 bit random, shift by 15 bits
jmp pull
pull8: mov ecx,23 ; For 8 bit random, shift by 23 bits
jmp pull
pull7: mov ecx,24 ; For 7 bit random, shift by 24 bits
jmp pull
pull6: mov ecx,25 ; For 6 bit random, shift by 25 bits
jmp pull
pull4: mov ecx,27 ; For 4 bit random, shift by 27 bits
pull: push ecx ; rand trashes ecx; save shift value on stack
call rand ; Call rand for random value; returned in eax
pop ecx ; Pop stashed shift value back into ECX
shr eax,cl ; Shift the random value by the chosen factor
; keeping in mind that part we want is in CL
ret ; Go home with random number in eax

seedit: push dword 0 ; Push a 32-bit null pointer to stack, since
; we don't need a buffer.
call time ; Returns time_t value (32-bit integer) in eax
add esp,4 ; Clean up stack
push eax ; Push time value in eax onto stack
call srand ; Time value is the seed value for random gen.
add esp,4 ; Clean up stack
ret ; Go home; no return values

newline:
mov ecx,10 ; We need a skip value, which is 10 minus the
sub ecx,eax ; number of newlines the caller wants.
add ecx,nl ; This skip value is added to the address of
push ecx ; the newline buffer nl before calling printf.
call printf ; Display the selected number of newlines
add esp,4 ; Clean up the stack
ret ; Go home
nl db 10,10,10,10,10,10,10,10,10,10,0

程序分析:
这个文件包含的几个函数在《32位汇编语言学习笔记(43)--生成随机数》一文中都有分析:
pull函数用于产生规定位数的随机数。
seedit函数用于设置随机数种子的初值。
newline函数用于根据eax值,打印换行符的个数,比如eax=1,就打印一个换行符。

textfile.asm是主程序:

[SECTION .data]			; Section containing initialised data

IntFormat dd '%d',0
WriteBase db 'Line #%d: %s',10,0
NewFilename db 'testeroo.txt',0
DiskHelpNm db 'helptextfile.txt',0
WriteCode db 'w',0
OpenCode db 'r',0
CharTbl db '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-@'
Err1 db 'ERROR: The first command line argument must be an integer!',10,0
HelpMsg db 'TEXTTEST: Generates a test file. Arg(1) should be the # of ',10,0
HELPSIZE EQU $-HelpMsg
db 'lines to write to the file. All other args are concatenated',10,0
db 'into a single line and written to the file. If no text args',10,0
db 'are entered, random text is written to the file. This msg ',10,0
db 'appears only if the file HELPTEXTFILE.TXT cannot be opened. ',10,0
HelpEnd dd 0

[SECTION .bss] ; Section containing uninitialized data

LineCount resd 1 ; Reserve integer to hold line count
HELPLEN EQU 72 ; Define length of a line of help text data
HelpLine resb HELPLEN ; Reserve space for disk-based help text line
BUFSIZE EQU 64 ; Define length of text line buffer buff
Buff resb BUFSIZE+5 ; Reserve space for a line of text

[SECTION .text] ; Section containing code

;; These externals are all from the glibc standard C library:
extern fopen
extern fclose
extern fgets
extern fprintf
extern printf
extern sscanf
extern time

;; These externals are from the associated library linlib.asm:
extern seedit ; Seeds the random number generator
extern pull6 ; Generates a 6-bit random number from 0-63
extern newline ; Outputs a specified number of newline chars

global main ; Required so linker can find entry point

main:
push ebp ; Set up stack frame for debugger
mov ebp,esp
push ebx ; Program must preserve EBP, EBX, ESI, & EDI
push esi
push edi
;;; Everything before this is boilerplate; use it for all ordinary apps!

call seedit ; Seed the random number generator

;; First test is to see if there are command line arguments at all.
;; If there are none, we show the help info as several lines. Don't
;; forget that the first arg is always the program name, so there's
;; always at least 1 command-line argument!
mov eax,[ebp+8] ; Load argument count from stack into EAX
cmp eax,1 ; If count is 1, there are no args
ja chkarg2 ; Continue if arg count is > 1
mov ebx,DiskHelpNm ; Put address of help file name in ebx
call diskhelp ; If only 1 arg, show help info...
jmp gohome ; ...and exit the program

;; Next we check for a numeric command line argument 1:
chkarg2:
mov ebx,[ebp+12] ; Put pointer to argument table into ebx
push LineCount ; Push address of line count integer for sscanf
push IntFormat ; Push address of integer formatting code
push dword [ebx+4] ; Push pointer to arg(1)
call sscanf ; Call sscanf to convert arg(1) to an integer
add esp,12 ; Clean up the stack
cmp eax,1 ; Return value of 1 says we got a number
je chkdata ; If we got a number, go on; else abort
mov eax,Err1 ; Load eax with address of error message #1
call showerr ; Show the error message
jmp gohome ; Exit the program

;; Here we're looking to see if there are more arguments. If there
;; are, we concatenate them into a single string no more than BUFSIZE
;; chars in size. (Yes, I *know* this does what strncat does...)
chkdata:
cmp dword [ebp+8],3 ; Is there a second argument?
jae getlns ; If so, we have text to fill a file with
call randline ; If not, generate a line of random text
; Note that randline returns ptr to line in esi
jmp genfile ; Go on to create the file

;; Here we copy as much command line text as we have, up to BUFSIZE
;; chars, into the line buffer buff. We skip the first two args
;; (which at this point we know exist) but we know we have at least
;; one text arg in arg(2). Going into this section, we know that
;; ebx contains the pointer to the arg table. All other bets are off.
getlns: mov edx,2 ; We know we have at least arg(2), start there
mov edi,Buff ; Destination pointer is start of char buffer
xor eax,eax ; Clear eax to 0 for the character counter
cld ; Clear direction flag for up-memory movsb

grab: mov esi,[ebx+edx*4] ; Copy pointer to next arg into esi
.copy: cmp byte [esi],0 ; Have we found the end of the arg?
je .next ; If so, bounce to the next arg
movsb ; Copy char from [esi] to [edi]; inc edi & esi
inc eax ; Increment total character count
cmp eax,BUFSIZE ; See if we've filled the buffer to max count
je addnul ; If so, go add a null to buff & we're done
jmp .copy

.next: mov byte [edi],' ' ; Copy space to buff to separate args
inc edi ; Increment destion pointer for space
inc eax ; Add one to character count too
cmp eax,BUFSIZE ; See if we've now filled buff
je addnul ; If so, go down to add a nul and we're done
inc edx ; Otherwise, increment the argument count
cmp edx, dword [ebp+8] ; Compare against argument count
jae addnul ; If edx = arg count, we're done
jmp grab ; And go back and copy it

addnul: mov byte [edi],0 ; Tuck a null on the end of buff
mov esi,Buff ; File write code expects ptr to text in esi

;; Now we create a file to fill with the text we have:
genfile:
push WriteCode ; Push pointer to file write/create code ('w')
push NewFilename ; Push pointer to new file name
call fopen ; Create/open file
add esp,8 ; Clean up the stack
mov ebx,eax ; eax contains the file handle; save in ebx

;; File is open. Now let's fill it with text:
mov edi,[LineCount] ; The number of lines to be filled is in edi

push esi ; esi is the pointer to the line of text
push 1 ; The first line number
push WriteBase ; Push address of the base string
push ebx ; Push the file handle of the open file

writeline:
cmp dword edi,0 ; Has the line count gone to 0?
je donewrite ; If so, go down & clean up stack
call fprintf ; Write the text line to the file
dec edi ; Decrement the count of lines to be written
add dword [esp+8],1 ; Update the line number on the stack
jmp writeline ; Loop back and do it again
donewrite:
add esp,16 ; Clean up stack after call to fprintf

;; We're done writing text; now let's close the file:
closeit:
push ebx ; Push the handle of the file to be closed
call fclose ; Closes the file whose handle is on the stack
add esp,4

;;; Everything after this is boilerplate; use it for all ordinary apps!
gohome: pop edi ; Restore saved registers
pop esi
pop ebx
mov esp,ebp ; Destroy stack frame before returning
pop ebp
ret ; Return control to to the C shutdown code

diskhelp:
push OpenCode ; Push pointer to open-for-read code "r"
push ebx ; Pointer to name of help file is passed in ebx
call fopen ; Attempt to open the file for reading
add esp,8 ; Clean up the stack
cmp eax,0 ; fopen returns null if attempted open failed
jne .disk ; Read help info from disk, else from memory
call memhelp
ret
.disk: mov ebx,eax ; Save handle of opened file in ebx
.rdln: push ebx ; Push file handle on the stack
push dword HELPLEN ; Limit line length of text read
push HelpLine ; Push address of help text line buffer
call fgets ; Read a line of text from the file
add esp,12 ; Clean up the stack
cmp eax,0 ; A returned null indicates error or EOF
jle .done ; If we get 0 in eax, close up & return
push HelpLine ; Push address of help line on the stack
call printf ; Call printf to display help line
add esp,4 ; Clean up the stack
jmp .rdln

.done: push ebx ; Push the handle of the file to be closed
call fclose ; Closes the file whose handle is on the stack
add esp,4 ; Clean up the stack
ret ; Go home

memhelp:
mov eax,1
call newline
mov ebx,HelpMsg ; Load address of help text into eax
.chkln: cmp dword [ebx],0 ; Does help msg pointer point to a null?
jne .show ; If not, show the help lines
mov eax,1 ; Load eax with number of newslines to output
call newline ; Output the newlines
ret ; If yes, go home
.show: push ebx ; Push address of help line on the stack
call printf ; Display the line
add esp,4 ; Clean up the stack
add ebx,HELPSIZE ; Increment address by length of help line
jmp .chkln ; Loop back and check to see if we done yet

showerr:
push eax ; On entry, eax contains address of error message
call printf ; Show the error message
add esp,4 ; Clean up the stack
ret ; Go home; no returned values

randline:
mov ebx,BUFSIZE ; BUFSIZE tells us how many chars to pull
mov byte [Buff+BUFSIZE],0 ; Put a null at the end of the buffer first
.loop: dec ebx ; BUFSIZE is 1-based, so decrement
call pull6 ; Go get a random number from 0-63
mov cl,[CharTbl+eax] ; Use random # in eax as offset into table
; and copy character from table into cl
mov [Buff+ebx],cl ; Copy char from cl to character buffer
cmp ebx,0 ; Are we done having fun yet?
jne .loop ; If not, go back and pull another
mov esi,Buff ; Copy address of the buffer into esi
ret ; and go home

程序分析:
main:
    push ebp  //保存栈帧指针
 mov ebp,esp  //ebp=esp,保存栈指针作为新的栈帧位置
 push ebx  //保存通用寄存器
 push esi
 push edi

 call seedit  //调用seedit函数,设置随机数种子
 
 mov eax,[ebp+8] //命令行参数个数放入eax
 cmp eax,1  //比较eax和1
 ja chkarg2  //如果命令行参数个数大于1,则跳转至chkarg2
 mov ebx,DiskHelpNm //ebx= DiskHelpNm
 call diskhelp  //如果命令行参数个数为1,调用diskhelp函数
 jmp gohome  //退出程序

chkarg2:    //命令行参数个数大于1,会跳转到这
 mov ebx,[ebp+12] //ebx=字符串指针数组地址
 push LineCount  //LineCount变量的地址
 push IntFormat     //格式化字符串地址,作为format参数
 push dword [ebx+4] //第一个命令行字符串参数的地址,作为buffer参数
 call sscanf  //调用sscanf函数,会把argv的第一个字符串参数当做一个整数来处理,保存到LineCount中。
 add esp,12  //清理栈
 cmp eax,1  //比较sscanf函数的返回值和1
 je chkdata  //如果eax=1跳转到chkdata
 mov eax,Err1  //否则eax= Err1
 call showerr  //调用showerr函数,显示出错信息
 jmp gohome  //退出程序

chkdata:
 cmp dword [ebp+8],3 //比较命令行参数的个数与3
 jae getlns  //如果大于等于3,表示有文本需要写入文件,跳转到getlns。
 call randline  //调用randline函数,随机产生一段文本。
 jmp genfile  //跳转到genfile,生成文件。

getlns: mov edx,2  //edx=2
 mov edi,Buff  //edi=Buff,目的缓存
 xor eax,eax  //eax=0
 cld   //内存地址变化方向从低到高

grab: mov esi,[ebx+edx*4] //esi=命令行参数字符串指针(从argv[2]开始)
.copy:  cmp byte [esi],0 //是否是空指针,如果是空指针,说明字符串已经到了结束。
 je .next  //如果是空指针,跳转到next
 movsb  //edi=esi,esi=esi+1,edi=edi+1
 inc eax   //eax=eax+1,字符串的字符数计数
 cmp eax,BUFSIZE  //比较eax和BUFSIZE(避免溢出)
 je addnul  //如果eax等于BUFSIZE,则跳转到addnul,结束拷贝。
 jmp .copy   //跳转到.copy,继续拷贝字符串。
 
.next: mov byte [edi],' ' //拷贝到edi一个空格
 inc edi   //edi=edi+1
 inc eax   //eax=eax+1,加了一个空格所以加1
 cmp eax,BUFSIZE  //比较eax和BUFSIZE(避免溢出)
 je addnul  //如果eax等于BUFSIZE,则跳转到addnul,结束拷贝。
 inc edx   //否则edx=edx+1
 cmp edx, dword [ebp+8]  //edx与命令行参数个数相比较
 jae addnul  //如果edx大于等于命令行参数个数,则跳转到addnul
 jmp grab  //跳转到grab处理下一个命令行参数字符串。

addnul: mov byte [edi],0 //edi=0,字符串用0结束
 mov esi,Buff  //esi = Buff

genfile:
 push WriteCode  // 'w'参数入栈,用于创建和写文件。
 push NewFilename  //文件名参数入栈
 call fopen  //调用fopen函数
 add esp,8  //清理栈
 mov ebx,eax  //文件句柄保存到ebx

 mov edi,[LineCount] //把要写的行数装入edi中

 push esi  // Buff参数,对应WriteBase的%s
 push 1  //第一行,对应WriteBase的%d
 push WriteBase  //格式化字符串地址
 push ebx  //文件句柄
 
writeline:
 cmp dword edi,0  //比较edi和0
 je donewrite  //如果等于0,表示写完,跳转到donewrite,清理栈
 call fprintf  //调用fprintf函数,把格式化字符串写入到文件。
 dec edi   //edi = edi-1
 add dword [esp+8],1 //更新栈上的行号信息
 jmp writeline  //跳转到writeline,继续循环
donewrite:  
 add esp,16  //清理栈

closeit: 
 push ebx  //文件句柄压入栈
 call fclose  //调用fclose函数
 add esp,4    //清理栈
 
gohome: pop edi   //恢复寄存器
 pop esi
 pop ebx
 mov esp,ebp  //恢复栈指针
 pop ebp         //恢复栈帧指针
 ret   

diskhelp:
 push OpenCode  //’r’
 push ebx  // DiskHelpNm文件名
 call fopen  //调用fopen函数,打开文件
 add esp,8  //清理栈
 cmp eax,0    //比较eax和0
 jne .disk  //eax不等于0,说明读取文件成功,跳转到.disk
 call memhelp //读取失败则调用memhelp函数
 ret
.disk: mov ebx,eax  //ebx=eax,保存文件句柄
.rdln: push ebx  //文件句柄压入堆栈(stream)
 push dword HELPLEN //从文件读取的字节数(n)
 push HelpLine  //读入到HelpLine(string)
 call fgets  //调用fgets函数,读入文件数据到缓存
 add esp,12  //清理栈
 cmp eax,0  //比较eax和0
 jle .done  //如果小于等于0跳转到.done(小于等于0意味着出错或读完文件)
 push HelpLine  //HelpLine压入栈
 call printf  //打印帮助信息
 add esp,4  //清理栈
 jmp .rdln     //继续循环

.done: push ebx  //文件句柄压入堆栈
 call fclose  //关闭文件
 add esp,4  //清理栈
 ret   

memhelp:
 mov eax,1
 call newline          //打印一个换行符
 mov ebx,HelpMsg  //ebx= HelpMsg
.chkln: cmp dword [ebx],0 //ebx与0比较
 jne .show  //如果不等于0,跳转到.show
 mov eax,1  //eax=1
 call newline  //打印一个换行符
 ret   
.show: push ebx  // ebx压入栈
 call printf  //打印一行帮助信息
 add esp,4  //清理栈
 add ebx,HELPSIZE //ebx=ebx+ HELPSIZE,HELPSIZE是一行帮助信息的长度
 jmp .chkln  //继续循环
 
showerr:
 push eax  //eax保存错误信息字符串地址
 call printf     //打印错误提示信息
 add esp,4  //清理栈
 ret   
 
randline: 
 mov ebx,BUFSIZE  //ebx= BUFSIZE
 mov byte [Buff+BUFSIZE],0  //Buff[BUFSIZE] = 0
.loop: dec ebx   //ebx = ebx -1
 call pull6  //产生6位随机数
 mov cl,[CharTbl+eax] //cl= CharTbl[eax],eax是产生的6位随机数,大小在0到63之间。
 mov [Buff+ebx],cl //保存cl到Buff[ebx]
 cmp ebx,0  //ebx与0比较
 jne .loop  //如果不等于0,继续循环
 mov esi,Buff  //esi=Buff
 ret 
 
makefile文件内容:

textfile: textfile.o linlib.o
gcc textfile.o linlib.o -o textfile
textfile.o: textfile.asm
nasm -f elf -g -F stabs textfile.asm
linlib.o: linlib.asm
nasm -f elf -g -F stabs linlib.asm

测试:

[root@bogon textfile]# make
nasm -f elf -g -F stabs textfile.asm
nasm -f elf -g -F stabs linlib.asm
gcc textfile.o linlib.o -o textfile
[root@bogon textfile]# ./textfile 5 hello world!
[root@bogon textfile]# cat testeroo.txt
Line #1: hello world!
Line #2: hello world!
Line #3: hello world!
Line #4: hello world!
Line #5: hello world!