There are many things to say here to build up my mind and make a sketch of the
skeleton for this procedure. One thing to consider is that I have to design a
pre-formatted string that makes it easy to cast the values of the registers
into it and to display the same on the screen. There are 14 registers
considering the flags, so I can put them in two lines. If I separate the
registers with two space characters then it happens that the first "x" of
register IP is 9 bytes away from the first "x" of the register FL (flags)
which is exactly the same byte-distance from the first "x" of register BP and
the first "x" of register SI (Fig. A).
|
Fig. A - pre-formatted string as it will appear on screen.
|
In order to display two lines, I need to insert two bytes corresponding to
/Lf (Line Feed 0x0A) and /Cr (Carriage Return 0x0D) so the trick
is to keep two-bytes equal-spacing between all registers which is a very
convenient way to store the string in memory. To achieve this, I introduce two
/SP (empty Space 0x20) characters between the end and beginning of
registers. Since I have equal spacing, it will be easy to implement an
algorithm that fills the pre-formatted string and that makes always the same
jump when it ends filling the information of one register and moves forward to
start filling the next one. The string must end with the byte 0x00 to
signalize that the
show_string
procedure has to stop sending characters to the screen. The sequence of the
registers in the pre-formatted string must follow the reverse sequence of
PUSHes done by
AP-PROBE
(last in – first out) as I want to keep the code as simple as possible. Now
that I have an idea of the pre-formatted string and a very primitive idea of
the mechanism the code may use, it is time to sketch the bare-bones skeleton
of the stack_to_string procedure.
stack_to_string:
################################################################################
#
# This procedure uses a for loop in 8086 assembly to take the status saved
# on the stack, convert the hex in ASCII and saves it in the pre-formatted
# string.
#
# INPUT: 14 parameters on the stack
# OUTPUT: ---
# REGISTER USAGE: ???
# THIS ONE CALLS: ???
#
################################################################################
############################
# adjust source pointer
# DS:[SI] starts at SS:[BP]
############################
##############################################################################
# adjust destination pointer
# ES:[DI] starts at the first "x" of "IP=xxxx" in the pre-formatted string
##############################################################################
###################################
# for each element on the stack
# (for 14 elements)
###################################
############################################
# take one element
# (each element is a word from the stack)
############################################
##################################################################
# convert hex word to ASCII and save inside pre-formatted string
##################################################################
########################################################################
# jump to the beginning of next register in the pre-formatted string
########################################################################
#########################
# next element
#########################
#####################
# end of procedure
#####################
RET
##########################
# pre-formatted string
##########################
formatted_str:
"IP=xxxx FL=xxxx AX=xxxx BX=xxxx CX=xxxx DX=xxxx BP=xxxx" /Lf /Cr
"SI=xxxx DI=xxxx CS=xxxx DS=xxxx ES=xxxx SS=xxxx SP=xxxx" /Lf /Cr /00
In the first attempt, I wrote the code down in a way that it seemed to be
reasonable. I started thinking about how I could implement the different
blocks and I realized that the block "convert hex word to ASCII and save
inside pre-formatted string" actually is a bit more complex than it may seem
at first glance. Every word is 16-bits long, and the hexadecimal
representation is done with 4 ASCII characters. This means that I have to
proceed with a conversion nibble by nibble so I reworked a bit the skeleton
and I made it more complex to account for this problem. In the second attempt,
you can see such a change.
stack_to_string:
################################################################################
#
# This procedure uses a for loop in 8086 assembly to take the status saved
# on the stack, convert the hex in ASCII and saves it in the pre-formatted
# string.
#
# INPUT: 14 parameters on the stack
# OUTPUT: ---
# REGISTER USAGE: ???
# THIS ONE CALLS: ???
#
################################################################################
############################
# adjust source pointer
# DS:[SI] starts at SS:[BP]
############################
##############################################################################
# adjust destination pointer
# ES:[DI] starts at the first "x" of "IP=xxxx" in the pre-formatted string
##############################################################################
###################################
# for each element on the stack
# (for 14 elements)
###################################
############################################
# take one element
# (each element is a word from the stack)
############################################
##################################################################
# convert hex word to ASCII and save inside pre-formatted string
##################################################################
####################
# for each nibble
####################
####################
# take a nibble
####################
################################
# convert the nibble to ASCII
################################
##################################
# save the ASCII in the string
##################################
#################
# next nibble
#################
########################################################################
# jump to the beginning of next register in the pre-formatted string
########################################################################
#########################
# next element
#########################
#####################
# end of procedure
#####################
RET
##########################
# pre-formatted string
##########################
formatted_str:
"IP=xxxx FL=xxxx AX=xxxx BX=xxxx CX=xxxx DX=xxxx BP=xxxx" /Lf /Cr
"SI=xxxx DI=xxxx CS=xxxx DS=xxxx ES=xxxx SS=xxxx SP=xxxx" /Lf /Cr /00
Finally, I felt confident about the working plan and I started coding it.
stack_to_string:
################################################################################
#
# This procedure uses a for loop in 8086 assembly to take the status saved
# on the stack, convert the hex in ASCII and saves it in the pre-formatted
# string.
#
# INPUT: 14 parameters on the stack
# OUTPUT: ---
# REGISTER USAGE: BP, SI, DI, CX, BX, DS, ES, AX
# THIS ONE CALLS: out_a_nibble, nibble_to_ASCII
#
################################################################################
############################
# adjust source pointer
# DS:[SI] starts at SS:[BP]
############################
mov bp, sp # save the stack pointer
mov si, bp # point at top of the stack
push ss
pop ds # DS = SS
##############################################################################
# adjust destination pointer
# ES:[DI] starts at the first "x" of "IP=xxxx" in the pre-formatted string
##############################################################################
mov ax, formatted_str: # MOV ES, formatted_str: doesn't exist and we need
# a work around
mov es, ax # ES = AX = formatted_str:
mov di, 3 # DI = 3, because the first "x" is 3 bytes ahead from
# start of string.
###################################
# for each element on the stack
# (for 14 elements)
###################################
push cx
mov cx, 14
element_on_stack:
############################################
# take one element
# (each element is a word from the stack)
############################################
lodsw # AX = DS:[SI]; SI = SI + 2
mov bx, ax # AX is going to be manipulated later on (nibble by nibble)
# so we keep a copy of its original value in BX
##################################################################
# convert hex word to ASCII and save inside pre-formatted string
##################################################################
####################
# for each nibble
####################
push cx
mov cx, 4
nibble_by_nibble:
####################
# take a nibble
####################
# BX contains already the source word to select the nibble from
# CX is the nibble index changing from 4 to 1 in the for loop
# the function "out_a_nibble" can be called immediatelly.
call out_a_nibble:
################################
# convert the nibble to ASCII
################################
# AL contains already the nibble to be converted
# the function "nibble_to_ASCII" can be called immediatelly.
call nibble_to_ASCII:
##################################
# save the ASCII in the string
##################################
# AL contains already the ASCII code to store.
stosb # ES:[DI] = AL; DI = DI + 1
#################
# next nibble
#################
loop nibble_by_nibble:
pop cx
########################################################################
# jump to the beginning of next register in the pre-formatted string
########################################################################
add di, 5 # pos.: 0 1 2 3 4 5 6 7 8 9 a b c d e f ...
# string format: A X = x x x x B X = x x x x ...
#########################
# next element
#########################
loop element_on_stack:
pop cx
#####################
# end of procedure
#####################
RET
##########################
# pre-formatted string
##########################
formatted_str:
"IP=xxxx FL=xxxx AX=xxxx BX=xxxx CX=xxxx DX=xxxx BP=xxxx" /Lf /Cr
"SI=xxxx DI=xxxx CS=xxxx DS=xxxx ES=xxxx SS=xxxx SP=xxxx" /Lf /Cr /00
As you can see, I planned to use two other sub-procedures: the first to
separate the word a nibble at a time (out_a_nibble
sub-procedure) and the second to convert the selected nibble into ASCII code
(nibble_to_ASCII
sub-procedure). I will plan the design of these procedures in such a way that
the first procedure
out_a_nibble
accepts inputs in the same format as they are created inside the FOR-loop that
calls it and, at the same time, it will set outputs exactly in the format that
the second procedure
nibble_to_ASCII
requires them to be. In this way, I don't have to perform any register
manipulation and/or input/output adaptation between the caller and the called
procedure in assembly.
Before moving on in the definition of the two other sub-procedures, I want to
spend some words on stack_to_string not really about what it does but
rather in the way it is written. As you see I massively use comments. You may
think I do it for you, but indeed I do it for me 😉. Moreover, although the
assembly doesn't support a structured syntax such as C with the use of
parenthesis, I do try to highlight the logical nests in the code by my writing
style as much as I can. Without it, I would be completely lost. I know by
experience that having with me the code written in this way is much like
having a very good drawing1
of the thing I want to build when I go the sample shop (DEBUG.EXE) and start
building it with my hands. Now the point is that this part of the job can be
made automatic using a piece of software called assembler which in turn
expects the code to follow a certain syntax that it can make interpretation of
and convert into machine code. But remember that I don't have any assembler so
I will learn and (maybe) "feel" what the job of an assembler software
looks like by doing the assembling by hands. Isn't that cool? I like it!
Another aspect I want to highlight is how to build up logical loops in x86
assembly language. The CPU has just "jumps" available; some of them are
conditionals and some are a very special one. Consider the
LOOP instruction: this is again an example of a
piece of software hard-wired inside the circuits of the CPU. To understand it,
you can consider the following instruction:
LOOP a_memory_location:. What it does is
equivalent to:
DEC CX
JE a_memory_location:
Typically we use the LOOP instruction combined with the usage of CX register
to build the equivalent logic of a FOR loop in 8086 CPU. So the translation of
the FOR loop logic in assembly looks something like this:
################
# high level
################
for (i=0 to 3) {
doSomething
}
##############
# Assembly
##############
push cx
mov cx, 4
loop_begins_here:
doSomething
loop loop_begins_here:
pop cx
The point is that the register CX is locked by the private use that the
LOOP instruction makes of it. Moreover, we
cannot exclude that CX had a value important to another piece of code somehow
or somewhere else, so we have to take care not to destroy this value and
instead preserve it on the stack. There is no guarantee that our FOR loop
logic is at the root level, chances are that this logic is already nested. For
this reason, it is a good practice to consider for granted that it is always
nested somewhere and use PUSH and
POP instruction at the begin and at the end of
the logic block.
################
# high level
################
for (i=0 to 3) {
for (j=0 to 5){
doSomething
}
}
##############
# Assembly
##############
push cx
mov cx, 4
1st_level:
push cx
mov cx, 6
2nd_level:
doSomething
loop 2nd_level:
pop cx
loop 1st_level:
pop cx
As you can see, if you write a block of code sticking to the rule of PUSHing
and POPing CX at the beginning and end of a logical block, you can nest it
without any problem. These kinds of simple rules are coded into the compilers
which use these rules in the operation of compiling: translate from high-level
language to assembly.
Comments
Post a Comment