0% found this document useful (0 votes)
21 views42 pages

SO Lab3

This document is a report for a laboratory work assignment on floppy disk I/O operations for students in the Software Engineering and Automation department at the Technical University of Moldova. The assignment requires students to form groups of 3-4 members and complete several tasks involving reading and writing data to floppy disks using assembly language programs. The programs must implement functions for transferring data between the keyboard, floppy disk, and RAM memory. The student groups must then submit a structured report including their source code and the procedure for creating a bootable floppy disk image to complete the assignment.

Uploaded by

Maria Procopii
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
0% found this document useful (0 votes)
21 views42 pages

SO Lab3

This document is a report for a laboratory work assignment on floppy disk I/O operations for students in the Software Engineering and Automation department at the Technical University of Moldova. The assignment requires students to form groups of 3-4 members and complete several tasks involving reading and writing data to floppy disks using assembly language programs. The programs must implement functions for transferring data between the keyboard, floppy disk, and RAM memory. The student groups must then submit a structured report including their source code and the procedure for creating a bootable floppy disk image to complete the assignment.

Uploaded by

Maria Procopii
Copyright
© © All Rights Reserved
We take content rights seriously. If you suspect this is your content, claim it here.
Available Formats
Download as PDF, TXT or read online on Scribd
You are on page 1/ 42

Ministry of Education, Culture and Research of the Republic of Moldova

Technical University of Moldova


Faculty of Computers, Informatics and Microelectronics
Department of Software Engineering and Automation

Report
For laboratory work No. 3
“Floppy Disk I/O operations”

Did by:
Maria Procopii
Irina Racovcena
Maria Leșnco
gr. FAF-212
Checked by:
Rostislav Călin

Chișinău – 2023
Task: All students will form teams of 3 members from the academic group. Group
composition will be approved by the teacher, including teams of 2 or 4 members.
Teams must be formed promptly, within the time limit announced by the teacher
during class time.

The objectives of the work given by the laboratory are focused on the acquisition
of working skills with floppy disk access methods, especially reading and writing
data, but not limited to them. The given procedures also extend to other permanent
data storage media such as HDD disks.

The total disk volume of 1474560 bytes will be structured in 96 logical blocks of
30 sectors each. Each student will be allocated an individual block of 15360 bytes,
for recording his data as follows. The block distribution is represented in the
included file ("Floppy space distribution.xlsx").

Requirements:
1. In the first and last sector of each student's block (on diskette), textual
information must be entered in the following format (without quotes):
"@@@FAF-21* First name NAME###". This text string must be duplicated 10
times without additional delimiters.
Examples:
@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad
URSU###@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad
URSU###@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad URSU###@@@FAF-212 Vlad
URSU###@@@FAF-212 Vlad URSU###

@@@FAF-211 Andreia-Cristina SIRETANU###@@@FAF-211 Andreia-Cristina


SIRETANU###@@@FAF-211 Andreia-Cristina SIRETANU###@@@FAF-211
Andreia-Cristina SIRETANU###@@@FAF-211 Andreia-Cristina
SIRETANU###@@@FAF-211 Andreia-Cristina SIRETANU###@@@FAF-211
Andreia-Cristina SIRETANU###@@@FAF-211 Andreia-Cristina
SIRETANU###@@@FAF-211 Andreia-Cristina SIRETANU###@@@FAF-211
Andreia-Cristina SIRETANU###

2. Create an assembly language program that will have the following functions:
- (KEYBOARD ==> FLOPPY) : Reading from the keyboard a string with a
maximum length of 256 characters (backspace correction should work) and
writing this string to the floppy "N" times, starting at address {Head, Track,
Sector }. After the ENTER key is detected, if the length of the string is
greater than 0 (zero), a blank line and then the newly entered string should
be displayed. The variables "N", "Head", "Track" and "Sector" must be read
visibly from the keyboard. After the disk write operation is finished, the
error code should be displayed on the screen.
- (FLOPPY ==> RAM) : Reading from floppy disk "N" sectors starting at
address {Head, Track, Sector} and transferring this data to RAM memory
starting at address {XXXX:YYYY}. After the read operation from the
diskette is finished, the error code should be displayed on the screen. After
the error code, the entire volume of data at address {XXXX:YYYY} that
was read from the disk should be displayed on the screen. If the displayed
data volume is larger than a video page, then it is necessary to implement
pagination by pressing the "SPACE" key. The variables "N", "Head",
"Track" and "Sector" as well as the address {XXXX:YYYY} must also be
read from the keyboard.
- (RAM ==> FLOPPY) : Writing to the floppy disk starting from the address
{Head, Track, Sector} a volume of "Q" bytes, from the RAM memory
starting from the address {XXXX:YYYY}. The data block of "Q" bytes
must be displayed on the screen, and after the disk write operation is
finished, the error code must be displayed on the screen.

3. After executing a function above, the program must be ready to execute the next
function (any of the 3 functions described above).

4. Compiled code should preferably not exceed 512 bytes. Otherwise, it is


necessary to implement the bypass of this restriction and finally to create the
bootable disk image that works in VirtualBox.

5. Each team must complete a structured report, including a conclusion, similar to


previous work. The report must include the complete and working source code, as
well as the complete (tested) procedure for compiling and producing the bootable
image that works correctly in VirtualBox. If the description of the procedure for
creating the bootable image is missing from the report, or if the procedure is not
complete or does not work (cannot be reproduced), the entire team risks receiving a
grade of 5(FIVE)!! For this reason, it is necessary for each team member to test the
procedure. In the report it is necessary to specify including the version of the
compiler used and all other components or dependencies that were used in the
build process of the bootable image.

6. The report must be uploaded strictly in PDF format !!

Implementation:
The bootloader is necessary so that the kernel, which has all the code for the menu
and its options, could be loaded, and executed. The bootloader should have the size
of 512 bytes of AA and 55 as the last two hexadecimal values.
Before jumping to the bootloader, let’s look at the utils folder.

print.asm
print_string_buffer:
; The function prints a string from the buffer
; stored in the SI register, it expects a
; null symbol to be found in the buffer.
; Parameters:
; si - memory offset to the buffer

pusha

.loop:
lodsb
or al, al
jz .done

; display character
mov ah, 0x0A
mov bh, 0x00
mov cx, 1
int 0x10

; move cursor
mov ah, 0x02
inc dl
int 0x10

jmp .loop

.done:
popa
ret
The function requires si register to be set. First the function is pushing all the
registers to the stack, then in a loop it loads a stored byte from the si register, while
incrementing it. If the loaded byte is zero, then the function terminates by popping
all the registers initial values from the stack, otherwise it displays a character,
updates the cursor and continues to loop once again, until it finds the zero byte.

The following function is used to the start-up message of the bootloader.


print_start_up_message:
pusha
mov dh, 0
mov dl, 0

mov si, start_up_message


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

popa
ret

init_window.asm
%define color_black 0
%define color_blue 1
%define color_green 2
%define color_cyan 3
%define color_red 4
%define color_magenta 5
%define color_orange 6
%define color_gray 7
%define color_yellow 14
%define color_white 15

At the top the macros for the colors are set. This is done by the use of the define
macro from nasm.
Next the function that paints the screen in black and magenta for the font, as well
as the function that sets up the cursor are found.
init_window:
pusha
.clear_screen:
; Int 0x10
; AH = 06h
; AL = number of lines by which to scroll up (00h = clear entire window)
; BH = attribute used to write blank lines at bottom of window
; CH, CL = row, column of window's upper left corner
; DH, DL = row, column of window's lower right corner

mov ax, 0x0600 ; AH = 6 = Scroll Window Up, AL = 0 = clear window


mov bh, color_black << 4 | color_magenta ; Attribute to clear screen with (White on
Red)
xor cx, cx ; Clear window from 0, 0
mov dx, 25 << 8 | 80 ; Clear window to 24, 80
int 0x10 ; Clear the screen

mov ah, 0x02 ; Set cursor


mov bh, 0x00 ; Page 0
mov dx, 0x00 ; Row = 0, col = 0
int 0x10

.set_custom_cursor:
; Int 0x10
; AH = 01h
; CH = start scan line of character matrix (0-1fH; 20H=no cursor)
; CL = end scan line of character matrix (0-1fH)

mov ax, 0x0100 ; AH = 1 = Set Cursor Shape & Size, AL = 0 = nothing


mov ch, 0x1 ; Sets the width of the cursor, the higher the thicker
mov cl, 0x10 ; Sets the height of the cursor, the less the higher
int 0x10
popa
ret

The first function uses the 06H function from the 10H set of BIOS interrupts, in
order to clear the screen, paint black with magenta color for the font. Afterwards
the cursor is set by the use of the 01H function from the 10H set of BIOS
interrupts.
disk.asm
disk_load:
; The function reads DH number of sectors
; into ES:BX memory location from drive DL
; Parameters:
; es:bx - buffer memory address

push dx ; store dx on stack for error handling later

mov ah, 0x02 ; INT 13H 02H, BIOS read disk sectors into memory
mov al, dh ; number of sectors
mov ch, 0x00 ; cylinder
mov dh, 0x00 ; head
mov cl, 0x02 ; start reading sector (2 is the sector after the bootloader)
int 0x13 ; BIOS interrupt for disk functions

jc disk_error ; checks if CF (carry flag) set to 1

pop dx ; restore dx value from stack


cmp dh, al ; checks dh (number of read sectors) vs al (number of desired read
sectors)
jne disk_error ; if not the desired amount of sectors were read, then error
jmp disk_success

The function requires es:bx registers to be set, this being the memory address
where the kernel will be loaded. At the beginning, the function is pushing to the
stack dx register, this is done in order to keep the initial value of the sectors that is
desired to be read, which can be handy later. After that by the use of the 02H
function from the 13H sets of BIOS interrupts a specified number of sectors are
read into the memory address. In the case of failure of the previous interrupt, a
jump will be performed to the disk_error function. Otherwise, the function will
continue to popping from the stack the previous value of the dx register and
compare it with the actual number of the read sectors, in the case of them not being
equal the same jump will be performed, and only in the case of passing all the
above checks successfully, a final jump to the disk_success will be done.

Knowing that the following labels are defined, the discussion of the last two
functions will continue:
DISK_ERROR_MESSAGE: db "ERROR: could not read from disk", 0
DISK_SUCCESS_MESSAGE: db "INFO: successfully loaded the disk", 0
The error handling is done by the following function:
disk_error:
mov si, DISK_ERROR_MESSAGE
call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

jmp disk_load

At first the function displayes the error message, updates the cursor afterwards and
continues with the jump to the disk_load function to try once again to read the
desired number of sectors from the disk.

When it successes the following function is run:


disk_success:

mov si, DISK_SUCCESS_MESSAGE


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

ret

It prints the message of successfully reading loading from the disk, updates the
cursor and returns to the caller of the disk_load function.
bootloader.asm
org 0x7C00

; save DL (drive number) value from the BIOS


mov [drive_number], dl

call init_window
call print_start_up_message

; set up DX for disk loading


mov dh, 0x10 ; number of sectors that will be loaded into memory
mov dl, [drive_number] ; drive number to load (0 = boot disk)

; set up ES:BX memory address to load sectors into


mov bx, 0x1000 ; load sector to memory address 0x1000
mov es, bx ; ES = 0x1000
mov bx, 0x0 ; ES:BX = 0x1000:0 (segment:offset)

; set up segment registers for RAM


mov ax, 0x1000
mov ds, ax ; data segment
mov es, ax ; extra segment
mov fs, ax
mov gs, ax
mov ss, ax ; stack segment

call disk_load ; loads kernel into memory


jmp 0x1000:0x0

%include "src/bootloader/utils/print.asm"
%include "src/bootloader/utils/disk.asm"
%include "src/bootloader/utils/init_window.asm"

start_up_message: db "INFO: Loading the kernel...", 0


drive_number: resb 8

times 510-($-$$) db 0
dw 0xAA55

At the beginning, we are defining the org directive which implies a memory
address, which will be the offset from which the bootloader should start. This
ensures the bootloader does not overlap with the BIOS.
After that the bootloader sets up things for later usage. The first thing that the
bootloader is doing is saving the drive number given set up by the BIOS, before it
forwards the control to the bootloader. After that the screen is cleared by the use of
the init_window function and the start-up message is printed on the screen. Next,
the dx register is set, by assigning the value 10H as the number of sectors that will
be loaded in the memory to the dh register, that being the approximate value of the
sectors that the kernel is occupying, as well as setting dl register to the value of the
drive number given by the BIOS previously.
As the kernel should also have an offset in memory, in order to not conflict with
the bootloader or other components, all other registers should also be set up prior
to the kernel loading.
After setting the segment registers, the entry point, i.e. the main logic of the
bootloader, is executed by calling the disk_load function, and making a far jump to
the segment where the kernel is loaded.
At the end of the bootloader the imports of the previously discussed files can be
found, as well as the start-up message label.
The last two lines of code, sign the bootloader with the AA 55 hex, while ensuring
that the size of it is 512 bytes.

const.asm
; signature.asm consts
signature: db "@@@FAF-212 Maria PROCOPII###", 0

KERNEL_MSG_SUCCESS: db "INFO: Kernel loaded.", 0


IO_ERROR_MSG: db "ERROR: could not write to disk.", 0
WRITING_MSG_INFO: db "INFO: writing to disk the signature...", 0
WRITING_MSG_SUCCESS: db "INFO: data has been written to the disk.", 0

; 1531 / 18 = 85 (tracks) + 1 (sectors)


start_track: equ 5 ; 85 (tracks) - 80 (tracks per side)
start_sector: equ 2 ; 1 + 1, since enumeration start with 1

; 1560 / 18 = 86 (tracks) + 12 (sectors)


end_track: equ 6 ; 86 (tracks) - 80 (tracks per side)
end_sector: equ 13 ; 12 + 1, since enumeration start with 1

; menu.asm consts
menu_prompt_option1: db "1. stdio to disk", 0
menu_prompt_option2: db "2. disk to ram", 0
menu_prompt_option3: db "3. ram to disk", 0
menu_prompt_option4: db "option := ", 0
opt_invalid: db "invalid", 0

; signature.asm vars
buffer: resb 100

The content of this file varies for all the users, as each member of the team is
assigned with his/her own sectors and signature, as described in the requirements.
The signature is the string that is associated with the student’s name and group.
The following four labels are used for printing debug information to the screen on
the runtime of the program. The start_track and start_sector are values that are
calculated from the given list in the task requirements, and are used to define the
first sector of the student. Similarly is with the end_track and end_sector.
The following labels are used to print the menu options and display the incorrectly
chosen one.
And the last label reserves a large amount of bytes to store the duplicated version
of the signature.

print.asm
print_string_buffer:
; The function prints a string from the buffer
; stored in the SI register, it expects a
; null symbol to be found in the buffer.
; Parameters:
; SI = memory offset to the buffer
; Returns:

pusha
.loop:
lodsb
or al, al
jz .done

; display character
mov ah, 0x0A
mov bh, 0x00
mov cx, 1
int 0x10

; move cursor
mov ah, 0x02
inc dl
int 0x10
jmp .loop

.done:
popa
ret

print_os_message:
; The function prints the OS message
; and moves the cursor to the next line
; Parameters: None
; Returns: None

pusha

; move cursor
mov ah, 0x02
mov dh, 2
mov dl, 0
int 0x10

mov si, KERNEL_MSG_SUCCESS


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

popa
ret

print_writing_to_disk_message_info:
pusha

; move cursor
mov ah, 0x02
mov dh, 3
mov dl, 0
int 0x10

mov si, WRITING_MSG_INFO


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10
popa
ret

print_writing_to_disk_message_success:
pusha

; move cursor
mov ah, 0x02
mov dh, 4
mov dl, 0
int 0x10

mov si, WRITING_MSG_SUCCESS


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

popa
ret

print_io_error:
pusha

; move cursor
mov ah, 0x02
mov dh, 4
mov dl, 0
int 0x10

mov si, IO_ERROR_MSG


call print_string_buffer

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

popa
jmp $

The function, called print_string_buffer, is a helper function that is used to print a


buffer’s content. This helper function is used in all other printing functions. The
function expects the register si to be set up beforehand. After pushing all the values
of the registers to the stack, a byte from the si register is loaded in the al register,
by the use of the lodsb instruction. The next line is checking if the value stored in
the al is zero, in that case the function is exiting, by jumping to the .done label.
Otherwise the char is printed to the screen and the cursor is updated. At the end the
loop is starting once again, until the zero byte in the si register is found.
All the other functions are updating the cursor first, then a message is printed by
the use of the print_string_buffer, at the end the function is updating the cursor
once again.

window.asm
clear_display:

; Int 0x10
; AH = 06h
; AL = number of lines by which to scroll up (00h = clear entire window)
; BH = attribute used to write blank lines at bottom of window
; CH, CL = row, column of window's upper left corner
; DH, DL = row, column of window's lower right corner

pusha

mov ax, 0x0600 ; AH = 6 = Scroll Window Up, AL = 0 = clear window


mov bh, 0 << 4 | 5 ; Attribute to clear screen with (magenta on black)
xor cx, cx ; Clear window from 0, 0
mov dx, 25 << 8 | 80 ; Clear window to 24, 80
int 0x10 ; Clear the screen

mov ah, 0x02 ; Set cursor


mov bh, 0x00 ; Page 0
mov dx, 0x00 ; Row = 0, col = 0
int 0x10

popa
ret

The above function first pushes all the registers values to the stack, after that uses
the 06H function from the 10H set of BIOS interrupts to clear the screen, then the
cursor is set at the top left corner, by the use of the 02H function from the 10H set
of BIOS interrupts. At the end the function is popping from the stack all the initial
values of the registers, after which the function returns to the caller.
signature.asm
create_signature:
; The function creates the signature, which
; is the given string (from SI) repeated 10
; times and written in the given buffer (from DI)
; The following register are set at the runtime
; SI = src (string, part of the signature)
; DI = dst (buffer where the signature will be written)
; CX = number of characters from signature
; Parameters:
; The function expects `buffer` and `signature` to be
; declared out of its scope.
; `buffer` = reserved bytes for the storage of the created signature
; `signature` = string that is part of the signature
; Returns: None

mov di, buffer


times 10 call .copy_signature ; copies the signature to the buffer 10 times
ret ; returns to out of scope caller

; subroutine
.copy_signature:
mov si, signature
mov cx, 25

.copy_string:
dec cx
jz .done

mov al, [si]


mov [di], al

inc si
inc di

jmp .copy_string

.done:
ret

sign_sector:
; The function write the signature to the
; desired address based on the CHS scheme
; Parameters:
; Int 13H
; AH = 03h
; AL = sector count
; CH = track (cylinder) number
; CL = sector number
; DH = head number
; DL = drive: 0-3=diskette; 80H-81H=hard disk
; ES:BX = caller's buffer, containing data to write
; Returns:
; AH = BIOS disk error code if CF is set to CY

pusha

mov ah, 0x03


mov al, 0x01
mov dh, 0x01
mov dl, 0x00
mov bx, buffer
int 0x13

jc print_io_error ; if CF is set

popa
ret

print_signature:
pusha

; move cursor
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

mov si, buffer


call print_string_buffer

popa
ret

The first function, called create_signature is responsible for duplicating the given
signature 10 times, as specified in the requirements. It also writes the result of it
into a buffer.
The function simply calls 10 times a subroutine in order to achieve this. The
subroutine called .copy_signature, copies from the register si into the register di as
many bytes as are specified in the cx register. It does that byte by byte, while
decreasing the number in cx, which is the length of the signature.

The next function called sign_sector is responsible for writing to the disk the
duplicated string from the buffer. It does that by the use of the 03H function from
the 13H set of BIOS interrupts.

And the last function called print_signature is a helper function that can be called
to view the duplicated signature.

menu.asm
display_menu:

; read character
mov ah, 0x00
int 0x16

cmp al, 0x20


je .cleared_screen

.clear_screen:
call clear_display
jmp display_menu

.cleared_screen:
xor ax, ax
xor dx, dx
call get_cursor_pos

mov ah, 0x02


inc dh
mov dl, 0
int 0x10

mov si, menu_prompt_option1


call print_string_buffer

mov ah, 0x02


inc dh
mov dl, 0
int 0x10

mov si, menu_prompt_option2


call print_string_buffer

mov ah, 0x02


inc dh
mov dl, 0
int 0x10

mov si, menu_prompt_option3


call print_string_buffer

mov ah, 0x02


inc dh
mov dl, 0
int 0x10

mov si, menu_prompt_option4


call print_string_buffer

; read character
mov ah, 0x00
int 0x16

; display character
mov ah, 0x0A
mov bh, 0x00
mov cx, 1
int 0x10

; move cursor
mov ah, 0x02
inc dl
int 0x10

cmp al, "1"


je handle_first_option

cmp al, "2"


je handle_second_option

cmp al, "3"


je handle_third_option

jmp handle_invalid_option
handle_first_option:
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

call first_option

jmp display_menu

handle_second_option:
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

call second_option

jmp display_menu

handle_third_option:
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

call third_option

jmp display_menu

handle_invalid_option:
mov ah, 0x02
inc dh
mov dl, 0
int 0x10

mov si, opt_invalid


call print_string_buffer

jmp display_menu

%include "src/kernel/utils/menu_options/common.asm"
%include "src/kernel/utils/menu_options/first_option.asm"
%include "src/kernel/utils/menu_options/second_option.asm"
%include "src/kernel/utils/menu_options/third_option.asm"
At first the display_menu function a character is input from the user. In the case of
the input character being a the Spacebar the cursor’s position is updated and the
program continues, otherwise the screen is cleared first. Afterwards the menu is
displayed, by firstly updating the cursor on the screen, with the help of the 02H
function from the 10H set of BIOS interrupts, and secondly an option is printed to
the screen, by the use of the print_string_buffer function. These instructions are
repeated a few times, one block of code for each option. After the printing of the
menu is done, an char is input from the user, then the given char is displayed on the
screen and the cursor is updated. The given char is compared to the following
chars: “1”, “2”, “3”; in the case of giving one of the previously mentioned chars, a
respective option will be executed, otherwise the invalid message is displayed.

common.asm
get_cursor_pos:
; The function obtains the current cursor position
; and the size/shape of the cursor for a specified video page.
; Parameters:
; AH = 03H
; BH = video page number
; Returns:
; CH = cursor starting scan-line
; CL = cursor ending scan-line
; DH = current row
; DL = current column

mov ah, 0x03


mov bh, [page_num]
int 0x10

ret

The above function is using the 03H function from the 10H set of BIOS interrupts,
which sets the cursor’s scan-line value into the cx register and the current row and
column in the dx register.

empty_buffer:
; The function clears the buffer set in SI register, by
; assigning 0s into it.
; Parameters:
; SI = buffer to be emptified
; DI = the same buffer from the SI register
; but with the desired offset.
; Returns: None
pusha
.empty_buffer_loop:
mov byte[si], 0
inc si
cmp si, di
jl .empty_buffer_loop

popa
ret

This function is setting all the buffer content to the zero byte, thus emptying it. It
does that by setting the current byte of the si register to zero, incrementing the si
register value afterwards, until the si value is not the same one the di has.

null_buffers:
; The function nulls out all the buffers used by the
; menu. The indented use of it, is to emptify all the
; buffers before their usage after a successful completion
; of an selected option.
; Parameters: None
; Returns: None

pusha

mov si, string


mov di, string + 256
call empty_buffer

mov si, string_buffer


mov di, string_buffer + 256
call empty_buffer

mov si, string_buffer_size


mov di, string_buffer_size + 4
call empty_buffer

mov si, nhts


mov di, nhts + 8
call empty_buffer

mov si, ram_address


mov di, ram_address + 4
call empty_buffer
mov si, storage_buffer
mov di, storage_buffer + 256
call empty_buffer

popa
ret

The above function nulls out all the buffers used by the program, by the use of the
previously described function.
read_input:
; The function reads the input and stores it into the `storage_buffer`,
; the user can edit the written input, by the use of backspace, and
; by typing enter, the input is read. The input shall be saved into
; another buffer.
; Parameters: None
; Returns:
; The input is stored in the `storage_buffer`.

mov si, storage_buffer


call get_cursor_pos

.read_char:
mov ah, 0x00
int 0x16

cmp al, 0x08


je .handle_backspace

cmp al, 0x0D


je .handle_enter

cmp si, storage_buffer + 256


je .read_char

mov [si], al
inc si

mov ah, 0x0E


int 0x10

jmp .read_char

.handle_backspace:
cmp si, storage_buffer
je .read_char

dec si
mov byte [si], 0

call get_cursor_pos

cmp dl, 0
je .previous_line

mov ah, 0x02


dec dl
int 0x10

mov ah, 0x0A


mov al, 0x20
int 0x10

jmp .read_char

.previous_line:
mov ah, 0x02
dec dh
mov dl, 79
int 0x10

mov ah, 0x0A


mov al, 0x20
int 0x10

jmp .read_char

.handle_enter:
cmp si, storage_buffer
je .read_char

mov byte [si], 0

ret

This function is a universal function for reading the input from the user. At the
beginning the function is reading a char, if the char is the Enter key then the
respective function handler is called, the similar behavior is encountered with the
Backspace key. Otherwise, the char is stored in the storage_buffer, and this char is
displayed on the screen. When the function encounters the limit of 255 bytes for
the string, i.e. more than 255 characters were given by the user, the only available
keys that are handled are the previously described, Enter and Backspace ones.

If the Backspace key is pressed, the routine first checks if any chars where given,
otherwise a jump to the .read_char is performed. If some chars where given, and
the Backspace was pressed, then the previous byte from the si register is set to
zero. Afterwards the cursor is updated. If the cursor is at the beginning of the line
then it jumps to the previous line, otherwise it moves to the left. At the end a space
in the place of the last char is displayed on the screen, and the loop repeats.

If the Enter key is pressed, the routine first checks if any chars where given,
otherwise a jump to the .read_char is performed. If some chars where given, the
zero byte is added to the buffer, to mark the end of the string and a return to the
caller is executed.

The following two helper functions are self-explanatory. And the algorithm are
exemplified in the doc-string of the function.
atoi:
; The function converts a string to integer. For example,
; "234" -> 234
; iteration 1:
; 1.1) "2" - "0" = 2 == ax
; 1.2) 0 * 10 = 0 == bx
; 1.3) 2 + 0 = 2 == bx
; iteration 2:
; 2.1) "3" - "0" = 3 == ax
; 2.2) 2 * 10 = 20 == bx
; 2.3) 3 + 20 = 23 == bx
; iteration 3:
; 3.1) "4" - "0" = 4 == ax
; 3.2) 23 * 10 = 230 == bx
; 3.3) 4 + 230 = 234 == bx
; Parameters:
; SI = src, buffer from where the string value is taken
; DI = dst, buffer where integer value is stored
; Returns: None

.atoi_loop:
cmp byte [si], 0
je .atoi_done
xor ax, ax
mov al, [si]
sub al, '0'

mov bx, [di]


imul bx, 10
add bx, ax
mov [di], bx

inc si
jmp .atoi_loop

.atoi_done:
ret

atoh:
; The function converts a string to integer. For example,
; "2C" -> 0x2C
; iteration 1:
; 1.1) "2" --dec--> 50
; 1.2) 50 is less than 65, thus jump to conversion of digit
; 1.3) al = 50-48 = 2
; 1.4) shift 0x0 to 0x00, and add 0x00 + 0x02 => 0x02
; 1.5) inc si, thus move the pointer in the string
; iteration 2:
; 2.1) "C" --dec--> 67
; 2.2) 67 is greater than 65, thus jump to conversion of letter
; 2.3) al = 67-55 = 12
; 2.4) shift 0x02 to 0x20, and add 0x20 + 0x12 => 0x32
; 2.5) inc si, thus move the pointer in the string
; Parameters:
; SI = src, buffer from where the string value is taken
; DI = dst, buffer where hexadecimal value is stored
; Returns: None

.atoh_loop:
cmp byte [si], 0
je .atoh_done

xor ax, ax
mov al, [si]
cmp al, 65
jl .conv_digit
.conv_letter:
sub al, 55
jmp .atoh_finish_iteration

.conv_digit:
sub al, 48

.atoh_finish_iteration:
mov bx, [di]
imul bx, 16
add bx, ax
mov [di], bx
inc si

jmp .atoh_loop

.atoh_done:
ret

The first one is used to convert a string to integer, and the second one is used to
convert a string to its hexadecimal equivalent.

read_nhts:
; The function handling input for N, Heads, Tracks, Sectors
; Parameters: None
; Returns: None

.read_n:
; display "N := "
call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, N_param_len
mov bp, N_param
int 0x10

; read user input `N`


call read_input

; convert ascii read to an integer


; and save to own buffer
mov di, nhts
mov si, storage_buffer
call atoi

.read_h:
; display "H := "
call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, H_param_len
mov bp, H_param
int 0x10

; read user input `H`


call read_input

; convert ascii read to integer


; and save to own buffer
mov di, nhts + 2
mov si, storage_buffer
call atoi

.read_t:
; display "T := "
call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, T_param_len
mov bp, T_param
int 0x10

; read user input `T`


call read_input

; convert ascii read to integer


; and save to own buffer
mov di, nhts + 4
mov si, storage_buffer
call atoi

.read_s:
; display "S := "
call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, S_param_len
mov bp, S_param
int 0x10

; read user input `S`


call read_input

; convert ascii read to integer


; and save to own buffer
mov di, nhts + 6
mov si, storage_buffer
call atoi

ret

The function above reads several parameters from the user. Each subroutine is
working in a similar way. First it updates the cursor, then displays the parameters
string, to inform the user which parameter to set. After a call to the read_input
function, which reads the given string, it is transformed to the appropriate format,
by the use of the atoi function.

read_ram_address:
; The function handling input for segment and offset
; Parameters: None
; Returns: None

; display "segment (XXXX) := "


call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, segment_param_len
mov bp, segment_param
int 0x10
; read user input `segment`
call read_input

; convert ascii read to hex


; and save to own buffer
mov di, ram_address
mov si, storage_buffer
call atoh

; display "offset (YYYY) := "


call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, offset_param_len
mov bp, offset_param
int 0x10

; read user input `offset`


call read_input

; convert ascii read to hex


; and save to own buffer
mov di, ram_address + 2
mov si, storage_buffer
call atoh

ret

Similar to the previously described function, this one is taking the segment and
offset parameters.

duplicate_string:
; The function is duplicating the given `string`
; Parameters:
; si = SRC, the buffer which stores the string that will be duplicated
; di = DST, the buffer that will store the duplicated string
; Returns: None

pusha

mov cx, 0
mov bx, word[nhts]
.get_string_length:
lodsb
or al, al
jz .get_string_length_done

inc cx
jmp .get_string_length

.get_string_length_done:
; store the the size of the storage
; for the duplicated string
pusha
mov ax, bx
mov dx, cx
mul dx
mov [string_buffer_size], ax
popa

mov si, string


inc cx
jmp .main

.main:
cmp bx, 0
je .done

jmp .copy_string

.copy_string:
push cx
.copy_string_loop:
dec cx
jz .copy_string_done

mov al, [si]


mov [di], al

inc si
inc di

jmp .copy_string_loop

.copy_string_done:
pop cx
dec bx
mov si, string
jmp .main

.done:
popa
ret

Used to duplicate a string N times, the function works as follows, first the function
is setting cx to zero, this register will hold the string length stored in the buffer. The
bx has the value of N, which defines how many times the string will be duplicated.
The .get_string_length subroutine is incrementing the value of cx, until the end of
the string is encountered in the buffer. When the subroutine is done, the size of the
duplicated string is stored in the string_buffer_size buffer. It does that by
multiplying the length of the string to the number of times the string will be
duplicated.
After that the function enters a loop that repeats until the bx value is zero, at each
iteration this value is decreased by one. At each iteration the .copy_string
subroutine is called. It first pushes the value of cx to the stack, then it copies the
string to the buffer byte by byte while decrementing the cx value. When the string
was successfully copied byte by byte the value of cx is restored from the stack, and
the next iteration is executed.
The function expects the si register to hold the value of the buffer with the string
that will be duplicated, while di register holds the value of the buffer where the
duplicated string will be stored.

print_error_code:
; The function print the error code to the
; user, after the complition of a I/O operation
; on the disk.
; Parameters: None
; Returns: None

push ax

call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05
mov cx, error_msg_len
mov bp, error_msg
int 0x10
pop ax

mov al, '0'


add al, ah
mov ah, 0x0E
int 0x10

ret

This function is displaying the error string that informs the user that the error code
is being displayed, after which the char value of the error code is displayed. That is
done by adding the decimal value of the ‘0’ char to the value of the error code,
after getting the ascii equivalent of the decimal error code, it is being displayed by
the help of the 0EH function from the 10H set of BIOS interrupts.

page_num: dw 0
string_param: db "string := ", 0
string_param_len: equ $ - string_param
N_param: db "N := ", 0
N_param_len: equ $ - N_param
H_param: db "H := ", 0
H_param_len: equ $ - H_param
T_param: db "T := ", 0
T_param_len: equ $ - T_param
S_param: db "S := ", 0
S_param_len: equ $ - S_param
segment_param: db "segment := ", 0
segment_param_len: equ $ - segment_param
offset_param: db "offset := ", 0
offset_param_len: equ $ - offset_param
error_msg: db "error code := ", 0
error_msg_len: equ $ - error_msg

string: resb 256


string_buffer: resb 256
string_buffer_size: resb 4
nhts: resb 8
ram_address: resb 4
storage_buffer: resb 256

The above declarations are declarations of the helper labels used all over the
previously described functions.

first_option.asm
first_option:
pusha
call null_buffers

; display "string := "


call get_cursor_pos
inc dh
mov dl, 0x00

mov ax, 0x1301


mov bl, 0x05 ; magenta color
mov cx, string_param_len
mov bp, string_param
int 0x10

; read user input `string`


call read_input

; save the `string` to its own buffer


mov si, storage_buffer
mov di, string
.copy_string_loop:
mov al, [si]
mov [di], al
inc si
inc di

cmp byte [si], 0


jne .copy_string_loop

; read user input `N`, `H`, `T`, `S`


call read_nhts

; prepare writing buffer


mov si, string
mov di, string_buffer
call duplicate_string

; calculate the number of sectors to write


xor dx, dx
mov ax, [string_buffer_size]
mov bx, 512
div bx
; write to the floppy
mov ah, 0x03
; mov al, 2
inc al
mov ch, [nhts + 4]
mov cl, [nhts + 6]
mov dh, [nhts + 2]
mov dl, 0x00
mov bx, string_buffer
int 0x13

; print error code


call print_error_code

; print writing buffer


call get_cursor_pos
mov ah, 0x02
inc dh
mov dl, 0x00
int 0x10

mov si, string_buffer


call print_string_buffer

popa
ret

First all the buffers should be null from the previous use, which is done by the call
to the null_buffers function.
After that the function displays an informative string to the screen, such that the
user does know which parameter is input next.
With the call to the read_input the given string is read. Then it is copied byte by
byte from the temporary buffer, called storage_buffer, to its own buffer, called
string. With the call of the read_nhts function, all the parameters are given for the
completion of the function.
The following block of code is duplicating the string as many times as the user
specified in the N parameter. After that the number of sectors needed to hold the
duplicated string is calculated, by dividing the previously calculated string buffer’s
size to the number of bytes of a single sector.
Next, the write to the floppy is executed by the use of the 03H function from the
13H set of BIOS interrupts. At the end, the error code setted by the previous
interrupt is displayed, as well as the duplicated string.

second_option:
pusha
call null_buffers

; read user input `ram address`


call read_ram_address

; read user input `N`, `H`, `T`, `S`


call read_nhts

; read data from floppy


push es
push bx

mov es, [ram_address]


mov bx, [ram_address + 2]

mov ah, 0x02


mov al, [nhts]
mov ch, [nhts + 4]
mov cl, [nhts + 6]
mov dh, [nhts + 2]
mov dl, 0x00
int 0x13

pop bx
pop es

; print error code


call print_error_code

popa
ret

The second option is similar to the previously described one, with the exception
that it takes the segment and offset from the user as one of the parameters, and
instead of writing to the floppy it reads from it, by the use of the 02H function from
the 13H set of BIOS interrupts.
third_option:
pusha
call null_buffers
; read user input `ram address`
call read_ram_address

; read user input `N`, `H`, `T`, `S`


call read_nhts

; calculate number of sectors to write


xor dx, dx
mov ax, [nhts]
mov bx, 512
div bx

; write data to floppy


push es
push bx

mov es, [ram_address]


mov bx, [ram_address + 2]

mov ah, 0x03


inc al
mov ch, [nhts + 4]
mov cl, [nhts + 6]
mov dh, [nhts + 2]
mov dl, 0x00
int 0x13

pop bx
pop es

; print error code


call print_error_code

popa
ret

And this option is the exact opposite of the second one, as it writes to the floppy
from the ram.

As all the helper function were described, the internal structure of the kernel can be
shown.
org 0

_start:
call print_os_message

call create_signature
; call print_signature

call print_writing_to_disk_message_info

mov ch, start_track


mov cl, start_sector
call sign_sector

mov ch, end_track


mov cl, end_sector
call sign_sector

call print_writing_to_disk_message_success

mov dl, 0 ; column


mov dh, 5 ; row
jmp display_menu

%include "src/kernel/utils/print.asm"
%include "src/kernel/utils/signature.asm"
%include "src/kernel/utils/window.asm"
%include "src/kernel/utils/menu.asm"
%include "src/kernel/utils/const.asm"

; sector padding
times 1474048-($-$$) db 0

At the very beginning the org directive defines that the kernel should start with an
offset of zero. At the very bottom, the imports and the padding can be found. The
padding ensures that the kernel’s size together with the first sector, occupied by the
bootloader, creates a file of the size of 1.44MB, which matches the 3.5 inches
floppy parameters. Looking at the _start function a message from the kernel is
printed, after which the signature is duplicated by the use of the create_signature
function. Then, the first and last sectors are marked, beforehand a message of trial
to mark them, will be printed, and after successfully signing the sectors, a message
will be displayed. At the end, the program enters an endless loop incorporated into
the menu.
Test:
The testing phase of the program is done via a makefile, it’s source code follows:
default: build run

build:
@nasm -f bin src/bootloader/bootloader.asm -o bin/boot.bin

@nasm -f bin src/kernel/kernel.asm -o bin/kernel.bin

@cat bin/boot.bin bin/kernel.bin > bin/os.bin

@dd if=/dev/zero of=bin/floppy.img bs=1024 count=1440


@dd conv=notrunc if=bin/os.bin of=bin/floppy.img

run:

@qemu-system-x86_64 -enable-kvm -drive


format=raw,file=bin/os.bin,index=0,if=floppy -m 4G -cpu host -smp 2 -vga virtio
-display sdl,gl=on -full-screen

hex-floppy:
@hexdump -C bin/floppy.img

hex-os:
@hexdump -C bin/os.bin

clean:
@rm -rfv bin/*.bin bin/*.img

Just by running make in the terminal will trigger the build and run targets. The first
one will build the program and the second one will run it, as expected.
In order to build the program, the user should have the following tool installed on
the system:
NASM version 2.16.01

The build target is compiling the bootloader first, and the kernel afterwards. It then
concatenates both binary files into one, by the use of the cat Linux util. In order to
facilitate the usage of the Virtual Box, a floppy image for it is created as well, by
the use of the dd Linux util.
The run target uses qemu, a virtual machine, usually available out of the box for
multiple Linux distros. The user should have the following tool installed on the
system:
QEMU emulator version 8.1.1

There are also helper targets. The clean target is responsible for deleting all the
files from the bin folder. The hex-floppy target will display the hexdump of the
floppy.img file. And the hex-os target will display the hexdump of the os.bin file. In
order to use these helper targets, the user should have the following tool installed
on the system:
hexdump from util-linux 2.39.2

Running make in the terminal will pop up the following window:

Figure 1. Initial virtual machine’s window.


By pressing Enter, the screen will clear, and by pressing Space the menu will be
displayed. Thus the window will have the following status:
Figure 2. Menu displayed in the virtual machine’s window.
The showcase of the first command follows:

Figure 3. The first command from the menu.

The showcase of the second command follows:


Figure 4. The second command from the menu.
The showcase of the third command follows:

Figure 5. The third command from the menu.


Conclusion:
In the lab, we tackled CHS and LBA storage addressing methods. CHS uses
cylinder, head, sector numbers, while LBA employs a simpler linear address.

We also dove into the 13H BIOS interrupts, which offer essential services for disk
operations in assembly language programming. This hands-on experience is key
for tasks like bootloading, file system work, and direct disk manipulation. Essential
knowledge for those venturing into operating systems or low-level software
development.

You might also like