本系列将讲解《汇编语言》一书,本节讲解**【汇编语言】17-1-手写字符串输入shell程序**。
本期源码,欢迎star:
https://github.com/ZYKWLJ/assembly4-homework
| 本节速览 |
|---|
| 1.整个shell程序如何判断输入字符的类别的? |
| 2.我们从键盘写入的字符放在了内存缓冲区的那一块? |
| 3.字符栈怎么实现的? |
| 4.BIOS的电传打字输出什么意思? |
| 5.程序有使用直接定址表吗?为什么可以使用?使用的优势是? |
| 6.画出整个程序的执行架构图? |
1.字符串输入shell程序介绍
我们进入dosbox后,都会有类似于C:\>这样的图标,提示你输入命令,并且输入的字符可以删除。

综上,我们要实现的shell程序,具有以下功能:

2.程序分析
我们编写的子程序,参数如下:

1.字符的输入和删除
维护一个字符栈,栈中的所有字符,从栈底到栈顶,组成一个字符串。支持压栈、出栈。
2.回车后,字符串输入结束
输入回车符后,可以在字符串中加入0,表示字符串结束。
3.在输入的同时需要显示这个字符串
每次有新的字符输入和删除一个字符的时候,都应该重新显示字符串栈底到栈顶,显示所有的字符。
4.程序处理过程
① 调用 int 16h 读取键盘输入;
② 如果是字符,进入字符栈,显示字符栈中的所有字符;继续执行①;
③ 如果是退格键,从字符栈中弹出一个字符,显示字符栈中的所有字符;继续执行①;
④ 如果是 Enter 键,向字符栈中压入 0,返回。
从程序的处理过程中可以看出,字符栈的入栈、出栈和显示栈中的内容,是需要在多处使用的功能,我们应该将它们写为子程序。

3.字符栈子程序
有上述分析,字符栈子程序如下:
;字符栈子程序实现
;程序:字符栈的入栈、出栈和显示。
;参数说明:
;(ah)=功能号,0表示入栈,1表示出栈,2表示显示:ds:si指向字符栈空间;
;对于0号功能:(al)=入栈字符;
;对于1号功能:(al)=返回的字符;
;对于2号功能:显示字符串,其中(dh)=行、(dl)=列
charstack:
jmp short charstart ;这是子程序 ` 标准写法 `,在数据定义前面写一条跳转指令,实现数据定义与首指令跳转和执行!
table dw charpush, charpop, charshow
;对应的入栈、出栈、显示
top dw 0 ;栈顶指针
charstart:
push bx ;子程序标准用法,保存自己会用到的寄存器
push dx
push di
push es
push ax
cmp ah, 2 ; 判断功能号是否大于2
ja sret ; 若大于2则直接返回
mov bl, ah ;ah存放的是功能号,注意一定是要bl,因为bx是基址寄存器,可以直接写在[]里面的,即[bx]。【ax:累加器、bx基址寄存器、cx计数寄存器、dx数据寄存器】
mov bh, 0
add bx, bx ; 计算功能号对应的偏移量,因为定址表是字大小,而我们的地址是字节。
jmp word ptr table[bx] ; 跳转到对应功能的子程序。这里使用了直接定制表的思想。
; 入栈子程序
charpush:
mov bx, top ;栈顶指针
mov [si][bx], al ; 将al中的字符存入栈中
inc top ; 栈顶指针加1
jmp sret
; 出栈子程序
charpop:
cmp top, 0 ; 判断栈是否为空
je sret ; 栈空则返回
dec top ; 栈顶指针减1
mov bx, top
mov al, [si][bx] ; 将栈顶字符存入al中
jmp sret
; 显示子程序
charshow:
mov bx, 0b800h ; 显存段地址
mov es, bx
mov al, 160
mov dh, 0
mul dh ; 计算行偏移(每行160字节),这里得到行显示的字节,ax里面是这个值
mov di, ax ;这是何故?将ax里面的行偏移字节转存到di中
add dl, dl ;这是列偏移,要加两次,因为每列两字节
mov dh, 0 ;这里又是干嘛呢?
add di, dx ;加上了列偏移,现在di中存的就是字节显示的屏幕的位置的全部的字节偏移了
mov bx, 0
charshowes:
cmp bx, top ; 判断是否显示完所有字符
jne noempty ;还有字符,就显示!
mov byte ptr es:[di], ' ' ; 栈空则显示空格
jmp sret
noempty: ;字符显示代码
mov al, [si][bx] ; 取出字符
mov es:[di], al ; 显示字符
mov byte ptr es:[di+2], ' ' ; 清除下一个位置的残留字符
inc bx
add di, 2 ; 移动到下一个显示位置
jmp charshowes
sret:
pop es ; 恢复寄存器
pop di
pop dx
pop bx
ret
字符栈的动态访问图示:

4.总代码
伪代码流程:
getstr:
push ax
getstrs:
mov ah,0
int 16h ;使用int 16读取键盘缓冲区的数据,该功能的编号为 0
cmp al 20h ;这里说明不是字符,这个值是32,对应的
jb nochar ;不是字符,查看是不是删除或者enter
mov ah, 0 ;是字符,先入栈
call charstack ;调用0号功能,字符入栈
mov ah, 2 ;再显示
call charstack ;调用2号功能,显示栈中的字符
jmp getstrs ;继续循环等待输入
nochar:
cmp ah, 0eh
je backspace
cmp ah, 1ch
je enter
jmp getstrs
backspace:
mov ah, 1
call charstack ;字符出栈
mov ah, 2
call charstack ;显示栈中的字符(因为不管入栈出栈,都要显示字符!)
jmp getstrs ;继续读取
enter: ;输入结束,开启下一段输出,继续显示prompt
mov al, 0
mov ah, 0
call charstack ;0入栈
mov ah, 2
call charstack ;显示栈中的字符
pop ax
ret
有了上述子程序,我们方便实现多了,不过,另外一个要注意的问题是,显示栈中字符的时候,要注意清除屏幕上上一次显示的内容。(这真的很容易忘记!)
可运行源码如下:
assume cs:code,ds:data
data segment
prompt db 'LinLin>','$'
buffer db 1000 dup(0)
top dw 0 ; 将top移到数据段
data ends
code segment
start:
mov ax, data
mov ds, ax
mov si, offset buffer ; 使用offset明确指定buffer位置
getstr:
; 每次输入字符前,都要先打印prompt `linlin`
prompt_display:
mov dx, offset prompt ; 显示提示符
mov ah, 9
int 21h
getstrs:
mov ah, 0
int 16h ; 使用int 16读取键盘缓冲区的数据
cmp al, 20h ; 对键入的字符进行判断,看是不是可视化字符?
jb nochar ; 不是字符,查看是不是删除或者enter
; 是字符,先入栈
mov ah, 0
call charstack
; 再显示
mov ah, 2
call charstack
jmp getstrs ; 继续循环等待输入
;==不是字符的情况处理==
nochar:
cmp ah, 0eh ; 退格键
je backspace
cmp ah, 1ch ; 回车键
je enter_key
jmp getstrs ; 其他键忽略,继续读取
backspace:
mov ah, 1
call charstack ; 字符出栈
mov ah, 2
call charstack ; 显示栈中的字符
jmp getstrs ; 继续读取
enter_key: ; 输入结束
; 检查退出条件 - 如果输入的是单个字符'q'
cmp top, 1
jne not_quit
mov bx, 0
mov al, [si+bx] ; 获取栈中第一个字符
cmp al, 'q'
je do_quit
cmp al, 'Q' ; 也检查大写Q
je do_quit
not_quit:
; 获取当前光标位置
mov ah, 3
mov bh, 0
int 10h
; dh = 行号, dl = 列号
; 换到下一行
inc dh ; 行号+1
mov dl, 0 ; 列号归0
mov ah, 2
int 10h ; 先将光标移到新行的行首
; 检查是否需要屏幕滚动(如果到达底部)
cmp dh, 25 ; 通常80x25文本模式
jb no_scroll
; 调用滚动功能
mov ah, 6 ; 向上滚动
mov al, 1 ; 滚动1行
mov bh, 7 ; 属性
mov ch, 0 ; 左上角行
mov cl, 0 ; 左上角列
mov dh, 24 ; 右下角行
mov dl, 79 ; 右下角列
int 10h
mov dh, 24 ; 设置到新底部行
no_scroll:
; 重置栈指针
mov top, 0
; 先将光标移到行首,再显示提示符
mov ah, 2
mov bh, 0
mov dl, 0
int 10h
jmp getstr ; 重新开始输入
do_quit:
; 返回DOS
mov ax, 4c00h
int 21h
; 字符栈子程序实现
charstack:
jmp short charstart
;直接定址表表项内容(存放着3个字符栈的操作)
table dw charpush, charpop, charshow
charstart:
push bx
push dx
push di
push es
push ax
cmp ah, 2
ja sret
mov bl, ah
mov bh, 0
add bx, bx
jmp word ptr table[bx]
; 入栈子程序
charpush:
cmp top, 1000 ; 防止缓冲区溢出
jae push_ret
mov bx, top
mov [si+bx], al
inc top
push_ret:
jmp sret
; 出栈子程序
charpop:
cmp top, 0
je sret
dec top
mov bx, top
mov al, [si+bx]
jmp sret
; 显示子程序
charshow:
; 保存当前光标位置
mov ah, 3
mov bh, 0
int 10h
push dx ; 保存光标位置
; 移动到提示符后的位置开始显示
mov ah, 2
mov bh, 0
mov dl, 7 ; "LinLin>" 长度是7,所以从第7列开始
int 10h
; 显示所有栈中的字符
mov bx, 0
cmp bx, top
je show_cursor ; 如果栈空,只显示光标
display_loop:
cmp bx, top
jae display_done
mov al, [si+bx]
mov ah, 0Eh ; BIOS 电传打字输出。模拟老式电传打字机(Teletype)的字符输出方式,每输出一个字符后,光标自动向右移动一格,当光标到达行尾时,自动移动到下一行行首。当光标到达屏幕底部时,整个屏幕向上滚动一行,输出字符时保持当前颜色和属性设置。
int 10h
inc bx
jmp display_loop
display_done:
; 显示闪烁光标(下划线)
mov al, ' '
mov ah, 0Eh
int 10h
show_cursor:
; 恢复光标位置到输入末尾
pop dx
mov ah, 2
mov bh, 0
; 计算新的列位置:7 + 字符数
mov dl, 7
add dl, byte ptr top
int 10h
sret:
pop ax
pop es
pop di
pop dx
pop bx
ret
code ends
end start
结果如下:
建议移步公粽号查看完整的视频。
从这里我们其实可以看出,使用汇编编写程序,我们最主要的是明确要开辟那几块内存?, 要操作哪一块内存,明确操作逻辑,各个寄存器分工,如果充分利用BIOS已有程序等等。

| 本节回顾 |
|---|
| 1.整个shell程序如何判断输入字符的类别的? |
| 2.我们从键盘写入的字符放在了内存缓冲区的那一块? |
| 3.字符栈怎么实现的? |
| 4.BIOS的电传打字输出什么意思? |
| 5.程序有使用直接定址表吗?为什么可以使用?使用的优势是? |
| 6.画出整个程序的执行架构图 |
本系列将精讲Linux0.11内核中的每一个文件,共计会发布100+文章。
😉【Linux102】11-kernel/vsprintf.c
😉【Linux102】12-include/stdarg.h
😉【Linux102】14-kernel/system_call.s
😉【Linux102】15-include/linux/sched.h
😉【Linux102】18-include/signal.h
😉【Linux102】19-include/sys/types.h
😉【Linux102】20-include/linux/kernel.h
😉【Linux102】21-include/asm/segment.h
😉【Linux102】22-include/linux/head.h
😉【Linux102】23-include/linux/mm.h
😉【Linux102】24-include/linux/fs.h
😉【Linux102】26-include/sys/wait.h
😉【Linux102】27-include/inux/tty.h
😉【Linux102】28-include/termios.h
😉【Linux102】30-include/sys/times.h
😉【Linux102】31-include/sys/utsname.h
😉【Linux102】32-include/stddef.h
😉【Linux102】33-include/linux/sys.h
😉【Linux102】36-include/asm/system.h
😉【Linux102】38-include/linux/fdreg.h
😉【Linux102】39-include/asm/io.h
😉【Linux102】40-kernel/blk_drv/blk.h
😉【Linux102】41-kernel/blk_drv/hd.c
😉【Linux102】42-include/linux/config.h
😉【Linux102】43-include/linux/hdreg.h
😉【Linux102】45-kernel/blk_drv/ramdisk.c
😉【Linux102】46-include/asm/memory.h
😉【Linux102】47-include/string.h
😉【Linux102】48-kernel/blk_drv/floppy.c
😉【Linux102】49-kernel/chr_drv/keyboard.S
😉【Linux102】50-kernel/chr_drv/console.c
😉【Linux102】51-kernel/chr_drv/serial.c
😉【Linux102】52-kernel/chr_drv/rs_io.s
😉【Linux102】53-kernel/chr_drv/tty_io.c
😉【Linux102】56-kernel/chr_drv/tty_ioctl.c
和Linux内核102系列不同,本系列将会从全局描绘Linux内核的各个模块,而非逐行源码分析,适合想对Linux系统有宏观了解的家人阅读。
😉【Linux】Linux概述1-linux对物理内存的使用
本系列将带领大家从0开始循序渐进学习汇编语言,直至完全掌握这门底层语言。同时给出学习平台DOSBox的使用教程。
😉【汇编练习】12-验证ROM和其他内存区域的OS的接管情况
本系列将直击C语言的本质基础,流利处理出各个易错、实用的实战点,并非从零开始学习C。
关于小希
😉嘿嘿嘿,我是小希,专注Linux内核领域,同时讲解C语言、汇编等知识。
我的微信:C_Linux_Cloud,期待与您学习交流!

加微信请备注哦
小希的座右铭:
别看简单,简单也是难。别看难,难也是简单。我的文章都是讲述简单的知识,如果你喜欢这种风格:
下一期想看什么?在评论区留言吧!我们下期见!

4442

被折叠的 条评论
为什么被折叠?



