Related
I have just started to learn assembly language at school, and as an exercise I have to make a program that calculate the sum of the first n integers (1+2+3+4+5+...+n).
I managed to build this program but during the comparison (line.9) I only compare the even numbers in register R1, so I would have to do another comparison for the odd numbers in R0.
MOV R0,#1 ; I put a register at 1 to start the sequence
INP R2,2 ; I ask the user up to what number the program should calculate, and I put its response in the R2 register
B myloop ; I define a loop
myloop:
ADD R1,R0,#1 ; I calculate n+1 and put it in register 1
ADD R3,R1,R0 ; I add R0 and R1, and I put the result in the register R3
ADD R0,R1,#1 ; I calculate n+2 and I put in the register R0, and so on...
ADD R4,R4,R3 ; R4 is the total result of all additions between R0 and R1
CMP R1,R2 ; I compare R1 and the maximum number to calculate
BNE myloop ; I only exit the loop if R1 and R2 are equal
STR R4,100 ; I store the final result in memory 100
OUT R4,4 ; I output the final result of the sequence
HALT ; I stop the execution of the program
I've tried several methods but I can't manage to perform this double comparison... (a bit like an "elif" in python)
Basically I would like to add this piece of code to also compare odd numbers:
CMP R0,R2
BNE myloop
But adding this like this directly after comparing even numbers doesn't work no matter if I put "BNE" or not.
You're trying to do a conjunction, in context, something like this:
do {
...
} while ( odd != n && even != n );
...
Eventually, one of those counters should reach the value n and stop the loop. So, both tests must pass in order to continue the loop. However, if either test fails, then the loop should stop.
First, we'll convert this loop into the if-goto-label form of assembly (while still using the C language!):
loop1:
...
if ( odd != n && even != n ) goto loop1;
...
Next, let's break down the conjunction to get rid of it. The intent of the conjunction is that if the first component fails, to stop the loop, without even checking the second component. However, if the first component succeeds, then go on to check the second component. And if the second also succeeds, then, and only then return to the top of the loop (knowing both have succeeded), and otherwise fall off the bottom. Either way, whether the first component fails or the second component fails, the loop stops.
This intent is fairly easy to accomplish in if-goto-label:
loop1:
...
if ( odd == n ) goto endLoop1;
if ( even != n ) goto loop1;
endLoop1:
...
Can you figure out how to follow this logic in assembly?
Another analysis might look like this:
loop1:
...
if ( odd == n || even == n ) goto endLoop1;
goto loop1;
endLoop1:
...
This is the same logic, but stated as how to exit the loop rather than how to continue the loop. The condition is inverted (De Morgan) but the intended target of the if-goto statement is also changed — it is effectively doubly-negated, so holds the same.
From that we would strive to remove the disjunction, again by making two statements instead of one with disjunction, also relatively straightforward using if-goto:
loop1:
...
if ( odd == n ) goto endLoop1;
if ( even == n ) goto endLoop1;
goto loop1;
endLoop1:
...
And with an optimization sometimes known as branch over unconditional branch (the unconditional branch is goto loop1;), we perform a pattern substitution, namely: (1) reversing the condition of the conditional branch, (2) changing the target of the conditional branch to the target of the unconditional branch, and (3) removing the unconditional branch.
loop1:
...
if ( odd == n ) goto endLoop1;
if ( even != n ) goto loop1;
endLoop1:
...
In summary, one takeaway is to understand how powerful the primitive if-goto is, and that it can be composed into conditionals of any complexity.
Another takeaway is that logic can be transformed by pattern matching and substitution into something logically equivalent but more desirable for some purpose like writing assembly! Here we work toward increasing use of simple if-goto's and lessor use of compound conditions.
Also, as #vorrade says, programs generally should not make assumptions about register values — so suggest to load 0 into the registers that need it at the beginning of the program to ensure their initialization. We generally don't clear registers after their use but rather set them before use. So, in another larger program, your code might run with other values from some other code left over in those registers.
Also, I answer the question posed, which is about compound conditionals, and explain how those work in some detail; though as stated elsewhere, there's no need to separate even and odd numbers in order to sum them, and, there's also a single formula that can compute the sum of numbers without iteration (though does require multiplication which may not be directly available and so would require a more bounded iteration..).
First of all your code assumes that R4 is 0 at the beginning.
This might not be true.
Your program becomes simpler and easier to understand if you add each number in a smaller loop, like this:
INP R2,2 ; I ask the user up to what number the program should calculate, and I put its response in the R2 register
MOV R0,#0 ; I put a register at 0 to start the sequence
MOV R4,#0 ; I put a register at 0 to start the sum
B testdone ; Jump straight to test if done
myloop:
ADD R0,R0,#1 ; I calculate n+1 and keep it in register 0
ADD R4,R4,R0 ; I add R4 and R0, and I put the result in the register R4
testdone:
CMP R0,R2 ; I compare R0 and the maximum number to calculate
BNE myloop ; I only exit the loop if R0 and R2 are equal
STR R4,100 ; I store the final result in memory 100
OUT R4,4 ; I output the final result of the sequence
HALT ; I stop the execution of the program
You only need 3 registers: R0 for current n, R2 for limit and R4 for the sum.
However, if you really have to add the even and odd numbers separately, you could do this way:
INP R2,2 ; I ask the user up to what number the program should calculate, and I put its response in the R2 register
MOV R0,#0 ; I put a register at 0 to start the sequence
MOV R4,#0 ; I put a register at 0 to start the sum
B testdone ; Jump straight to test if done
myloop:
ADD R0,R0,#1 ; I calculate n+1 (odd numbers), still register 0
ADD R4,R4,R0 ; I add R4 and R0, keep the result in the register R4
CMP R0,R2 ; I compare R0 and the maximum number to calculate
BEQ done ; I only exit the loop if R0 and R2 are equal
ADD R0,R0,#1 ; I calculate n+1 (even numbers) and keep it in register 0
ADD R4,R4,R0 ; I add R4 and R0, and I put the result in the register R4
testdone:
CMP R0,R2 ; I compare R0 and the maximum number to calculate
BNE myloop ; I only exit the loop if R0 and R2 are equal
done:
STR R4,100 ; I store the final result in memory 100
OUT R4,4 ; I output the final result of the sequence
HALT ; I stop the execution of the program
First of all I would like to thank you very much #vorrade , #Erik Eidt and #Peter Cordes, I read your comments and advice very carefully, and they are very useful to me :)
But in fact following the post of my question I continued to seek by myself a solution to my problem, and I came to develop this code which works perfectly!
// Date : 31/01/2022 //
// Description : A program that calculate the sum of the first n integers (1+2+3+4+5+...+n) //
MOV R0,#1 // I put a register at 1 to start the sequence of odd number
INP R2,2 // I ask the user up to what number the program should calculate, and I put its response in the R2 register
B myloop // I define a main loop
myloop:
ADD R1,R0,#1 // I calculate n+1 (even) and I put in the register R1, and so on...
ADD R3,R1,R0 // I add R0 and R1, and I put the result in the register R3
ADD R4,R4,R3 // R4 is the total result of all additions between R0 and R1, which is the register that temporarily stores the calculated results in order to increment them later in register R4
ADD R0,R1,#1 // I calculate the next odd number to add to the sequence
B test1 // The program goes to the first comparison loop
test1:
CMP R0,R2 // I compare the odd number which is in the current addition with the requested maximum, this comparison can only be true if the maximum is also odd.
BNE test2 // If the comparison is not equal, then I move on to the next test which does exactly the same thing but for even numbers this time.
ADD R4,R4,R2 // If the comparison is equal, then I add a step to the final result because my main loop does the additions 2 by 2.
B final // The program goes to the final loop
test2:
CMP R1,R2 // I compare the even number which is in the current addition with the requested maximum, this comparison can only be true if the maximum is also even.
BNE myloop // If the comparison is not equal, then the program returns to the main loop because this means that all the comparisons (even or odd) have concluded that the maximum has not yet been reached and that it is necessary to continue adding.
B final // The program goes to the final loop
final:
STR R4,100 // I store the final result in memory 100
OUT R4,4 // I output the final result of the sequence
HALT // I stop the execution of the program
I made some comments to explain my process!
I now realize that the decomposition into several loops was indeed the right solution to my problem, it allowed me to better realize the different steps that I had first written down on paper.
I am working on the Simple-Compiler project in Deitel's book C how to program. Its main goal is to generate a compiler for an advanced language called SIMPLE and the relevant machine language is called SIMPLETRON.
I've completed some basic features for this compiler but am now stuck with an enhanced requirement -- to realize gosub and return (subroutine features) for SIMPLE language.
The main obstacle here is that SIMPLETRON doesn't support indirect addressing, which means the strategy to use stack for returning addresses of subroutines can't work. In this case, is it possible to somehow make subroutines work?
PS: I searched this issue and found an relevant question here. It seemed self-modifying code might be the answer, but I failed to find specific resolutions and thus I still raised this question. Moreover in my opinion machine instructions for SIMPLETRON has to be extended to make self-modifying code work here, right?
Background information for SIMPLETRON machine language:
It includes only one accumulator as register.
All supported machine instructions as below:
Input/output operations
#define READ 10: Read a word from the terminal into memory and with an operand as the memory address.
#define WRITE 11: Write a word from memory to the terminal and with an operand as the memory address.
Load/store operations
#define LOAD 20: Load a word from memory into the accumulator and with an operand as the memory address.
#define STORE 21: Store a word from the accumulator into memory and with an operand as the memory address.
Arithmetic operations
#define ADD 30: Add a word from memory to the word in the accumulator (leave result in accumulator) and with an operand as the
memory address.
#define SUBTRACT 31: Subtract a word ...
#define DIVIDE 32: Divide a word ...
#define MULTIPLY 33: Multiply a word ...
Transfer of control operations
#define BRANCH 40: Branch and with an operand as the code location.
#define BRANCHNEG 41: Branch if the accumulator is negative and with an operand as the code location.
#define BRANCHZERO 42: Branch if the accumulator is zero and with an operand as the code location.
#define HALT 43: End the program. No operand.
I'm not familiar with SIMPLE or SIMPLETRON, but in general I can think of at least 3 approaches.
Self-modifying code
Have a BRANCH 0 instruction at the end of each subroutine, and before that, code to load the return address into the accumulator and STORE it into the code itself, thus effectively forming a BRANCH <dynamic> instruction.
Static list of potential callers
If SIMPLE doesn't have indirect calls (i.e. every gosub targets a statically known subroutine), then the compiler knows the list of possible callers of each subroutine. Then it could have each call pass a unique argument (e.g. in the accumulator), which the subroutine can test (pseudocode):
SUBROUTINE:
...
if (arg == 0)
branch CALLER_1;
if (arg == 1)
branch CALLER_2;
if (arg == 2)
branch CALLER_3;
Inlining
If SIMPLE doesn't allow recursive subroutines, there's no need to implement calls at the machine code level at all. Simply inline every subroutine into its caller completely.
Yes, you can do this, even reasonably, without self-modifying code.
You turn your return addresses into a giant case statement.
The secret is understanding that a "return address" is just a way
to get back to point of the call, and that memory is just a giant
array of named locations.
Imagine I have a program with many logical call locations, with the instruction
after the call labelled:
CALL S
$1: ...
...
CALL T
$2: ...
...
CALL U
$3: ...
We need to replace the CALLs with something our machine can implement.
Let's also assume temporarily that only one subroutine call is active at any moment.
Then all that matters, is that after a subroutine completes, that control
returns to the point after the call.
You can cause this by writing the following SIMPLETRON code (I'm making up the syntax). By convention I assume I have a bunch of memory locations K1, K2, ... that contain the constants 1, 2, .. etc for as many constants as a I need.
K1: 1
K2: 2
K3: 3
...
LOAD K1
JMP S
$1: ...
...
LOAD K2
JMP T
$2: ...
...
LOAD K3
JMP U
$3:....
S: STORE RETURNID
...
JMP RETURN
T: STORE RETURNID
...
JMP RETURN
U: STORE RETURNID
...
JMP RETURN
RETURN: LOAD RETURNID
SUB K1
JE $1
LOAD RETURNID
SUB K2
JE $2
LOAD RETURNID
SUB K3
JE $3
JMP * ; bad return address, just hang
In essence, each call site records a constant (RETURNID) unique to that call site, and "RETURN" logic uses that unique ID to figure out the return point. If you have a lot of subroutines, the return logic code might be quite long, but hey, this is a toy machine and we aren't that interested in efficiency.
You could always make the return logic into a binary decision tree; then
the code might be long but it would only take log2(callcount) to decide how to get back, not actually all that bad).
Let's relax our assumption of only one subroutine active at any moment.
You can define for each subroutine a RETURNID, but still use the same RETURN code. With this idea, any subroutine can call any other subroutine. Obviously these routines are not-reentrant, so they can't be called more than once in any call chain.
We can use this same idea to implement a return stack. The trick is to recognize that a stack is merely a set of memory locations with an address decoder that picks out members of the stack. So, lets implement
PUSH and POP instructions as subroutines. We change our calling convention
to make the caller record the RETURNID, leaving the accumulator free
to pass a value:
LOAD K1
STORE PUSHRETURNID
LOAD valuetopush
JMP PUSH
$1:
LOAD K2
STORE POPRETURNID
JMP POP
$2:...
TEMP:
STACKINDEX: 0 ; incremented to 1 on first use
STACK1: 0 ; 1st stack location
...
STACKN: 0
PUSH: STORE TEMP ; save value to push
LOAD PUSHRETURNID ; do this here once instead of in every exit
STORE RETURNID
LOAD STACKINDEX ; add 1 to SP here, once, instead of in every exit
ADD K1
STORE STACKINDEX
SUB K1
JE STORETEMPSTACK1
LOAD STACKINDEX
SUB K2
JE STORETEMPSTACK2
...
LOAD STACKINDEX
SUB Kn
JE STORETEMPSTACKn
JMP * ; stack overflow
STORETEMPSTACK1:
LOAD TEMP
STORE STACK1
JMP RETURN
STORETEMPSTACK2:
LOAD TEMP
STORE STACK2
JMP RETURN
...
POP: LOAD STACKINDEX
SUB K1 ; decrement SP here once, rather than in every exit
STORE STACKINDEX
LOAD STACKINDEX
SUB K0
JE LOADSTACK1
LOAD STACKINDEX
SUB K1
JE LOADSTACK2
...
LOADSTACKn:
LOAD STACKn
JMP POPRETURN
LOADSTACK1:
LOAD STACK1
JMP RETURNFROMPOP
LOADSTACK2:
LOAD STACK2
JMP RETURNFROMPOP
RETURNFROMPOP: STORE TEMP
LOAD POPRETURNID
SUB K1
JE RETURNFROMPOP1
LOAD POPRETURNID
SUB K2
JE RETURNFROMPOP2
...
RETURNFROMPOP1: LOAD TEMP
JMP $1
RETURNFROMPOP2: LOAD TEMP
JMP $2
Note that we need RETURN, to handle returns with no value, and RETURNFROMPOP, that handles returns from the POP subroutine with a value.
So these look pretty clumsy, but we can now realize a pushdown stack
of fixed but arbitrarily large depth. If we again make binary decision trees out the stack location and returnID checking, the runtime costs are only logarithmic in the size of the stacks/call count, which is actually pretty good.
OK, now we have general PUSH and POP subroutines. Now we can make calls that store the return address on the stack:
LOAD K1 ; indicate return point
STORE PUSHRETURNID
LOAD K2 ; call stack return point
JMP PUSH
$1: LOAD argument ; a value to pass to the subroutine
JMP RECURSIVESUBROUTINEX
; returns here with subroutine result in accumulator
$2:
RECURSIVESUBROUTINEX:
...compute on accumulator...
LOAD K3 ; indicate return point
STORE PUSHRETURNID
LOAD K4 ; call stack return point
JMP PUSH
$3: LOAD ... ; some revised argument
JMP RECURSIVESUBROUTINEX
$4: ; return here with accumulator containing result
STORE RECURSIVESUBROUTINERESULT
LOAD K5
STORE POPRETURNID
JMP POP
$5: ; accumulator contains return ID
STORE POPRETURNID
LOAD RECURSIVESUBROUTINERESULT
JMP RETURNFROMPOP
That's it. Now you have fully recursive subroutine calls with a stack, with no (well, faked) indirection.
I wouldn't want to program this machine manually because building the RETURN routines would be a royal headache to code and keep right. But a compiler would be perfectly happy to manufacture all this stuff.
Although there's no way to get the current instruction's location from within the SIMPLE instruction set, the assembler can keep track of instruction locations in order to generate the equivalent of return instructions.
The assembler would generate a branch to address instruction in the program image to be used as a return instruction, then to implement a call it would generate code to load a "return instruction" and store it at the end of a subroutine before branching to that subroutine. Each instance of a "call" would require an instance of a "return instruction" in the program image. You may want to reserve a range of variable memory to store these "return instructions".
Example "code" using a call that includes the label of the return instruction as a parameter:
call sub1, sub1r
; ...
sub1: ; ...
sub1r: b 0 ;this is the return instruction
Another option would be something similar to MASM PROC and ENDP, where the ENDP would hold the return instruction. The call directive would assume that the endp direction holds the branch to be modified and the label would be the same as the corresponding proc directive.
call sub1
; ...
sub1 proc ;subroutine entry point
; ...
sub1 endp ;subroutine end point, "return" stored here
The issue here is that the accumulator would be destroyed by the "call" (but not affected by the "return"). If needed, subroutine parameters could be stored as variables, perhaps using assembler directives for labeling:
sub1 parm1 ;parameter 1 for sub1
;....
load sub1.parm1 ;load sub1 parameter 1
I've just started learning Assembly and I got stuck now...
%include 'io.inc'
global main
section .text
main:
; read a
mov eax, str_a
call io_writestr
call io_readint
mov [nb_array], eax
call io_writeln
; read b
mov eax, str_b
call io_writestr
call io_readint
mov [nb_array + 2], eax
call io_writeln
mov eax, [nb_array]
call io_writeint
call io_writeln
mov eax, [nb_array + 2]
call io_writeint
section .data
nb_array dw 0, 0
str_a db 'a = ', 0
str_b db 'b = ', 0
So, I have a 2 elem sized array and when I try to print the first element, it doesn't print the right value. Although I try to print the second element, it prints the right value. Could someone help me understand why is this happening?
The best answer is probably "because there are no arrays in Assembly". You have computer memory available, which is addressable by bytes. And you have several instructions to manipulate those bytes, either by single byte, or by groups of them forming "word" (two bytes) or "dword" (four bytes), or even more (depends on platform and extended instructions you use).
To use the memory in any "structured" way in Assembly: it's up to you to write piece of code like that, and it takes some practice to be accurate enough and to spot all bugs in debugger (as just running the code with correct output doesn't mean much, if you would do only single value input, your program would output correct number, but the "a = " would be destroyed anyway - you should rather every new piece of code walk instruction by instruction in debugger and verify everything works as expected).
Bugs in similar code were so common, that people rather used much worse machine code produced by C compiler, as the struct and C arrays were much easier to use, not having to guard by_size multiplication of every index, and allocating correct amount of memory for every element.
What you see as result is exactly what you did with the memory and particular bytes (fix depends whether you want it to work for 16b or 32b input numbers, you either have to fix instructions storing/reading the array to work with 16b only, or fix the array allocation and offsets to accompany two 32b values).
I'm trying to copy array A into array N and then print the array (to test that it has worked) but all it outputs is -1
Here is my code:
ORG $1000
START: ; first instruction of program
clr.w d1
movea.w #A,a0
movea.w #N,a2
move.w #6,d2
for move.w (a0)+,(a2)+
DBRA d2,for
move.w #6,d2
loop
move.l (a2,D2),D1 ; get number from array at index D2
move.b #3,D0 ; display number in D1.L
trap #15
dbra d2,loop
SIMHALT ; halt simulator
A dc.w 2,2,3,4,5,6
N dc.l 6
END START ; last line of source
Why is -1 in the output only? If there is a better solution for this that would be very helpful
Since I don't have access to whatever assembler/simulator you're using, I can't actually test it, but here a few things (some of which are already noted in the comments):
dc.l declares a single long, you want ds.l (or similar) to allocate storage for 6 longs
dbra branches until the operand is equal to -1, so you'll probably want to turn
movw #loop_times, d0
loop
....
dbra d0, loop
into
movw #loop_times-1, d0
loop
....
dbra d0, loop
(this works as long as loop_times is > 0, otherwise you'll have to check the condition before entering the loop)
You display loop has a few problems: 1. On entry a2 points past the end of the N array. 2. Even fixing that, the way you're indexing it will cause problems. On the first entry you're trying to fetch a 4-byte long from address a2 + 6,then a long from a2 + 5...
What you want is to fetch longs from address a2 + 0, a2 + 4 .... One way of doing that:
move.w #6-1, d2 ; note the -1
movea.l #N, a2
loop
move.l (a2)+,D1 ; get next number from array
; use d1 here
dbra d2,loop
As already pointed out, your new array is only 4 bytes in size, you should change
dc.l 6 to ds.w 6
and also you work on 7 elements, since DBRA counts down to -1.
Second, and thats why you get -1 everywhere, you use A2 as pointer to the new array, but you do not reset it to point at the first word in new array. Since you increased it by one word per element during the copy, after the for loop has completed, A2 points to the first word after the array.
Your simulator outputting more than one number with your display loop indicates that your simulator does not emulate an MC68000, a real MC68000 would take a trap at "MOVE.L (A2,D2),D1" as soon as the sum of A2+D2 is odd - the 68000 does not allow W/L sized accesses to odd addresses (MC68020 and higher do).
A cleaned MC68000 compatible code could look like this:
lea A,a0
lea N,a2
moveq #5,d2
for move.w (a0)+,(a2)+
dbra d2,for
lea N,a2
moveq #5,d2
loop
move.w (a2)+,D1 ; get number (16 bits only)
ext.l d1 ; make the number 32 bits
moveq #3,D0 ; display number in D1.L
trap #15
dbra d2,loop
It probably contains some instructions you haven't encountered yet.
I have to come up with an ASM code (for emu8086) that will find the minimum and maximum value in an array of any given size. In the sample code, my instructor provides (what appears to be) a data segment that contains an array named LIST. He claims that he will replace this list with other lists of different sizes, and our code must be able to handle it.
Here's the sample code below. I've highlighted the parts that I've added, just to show you that I've done my best to solve this problem:
; You may customize this and other start-up templates;
; The location of this template is c:\emu8086\inc\0_com_template.txt
org 100h
data segment
LIST DB 05H, 31H, 34H, 30H, 38H, 37H
MINIMUM DB ?
MAXIMUM DB ?
AVARAGE DB ?
**SIZE=$-OFFSET LIST**
ends
stack segment **;**
DW 128 DUP(0) **; I have NO CLUE what this is supposed to do**
ends **;**
code segment
start proc far
; set segment registers:
MOV AX,DATA **;**
MOV DS,AX **;I'm not sure what the point of this is, especially since I'm supposed to be the programmer, not my teacher.**
MOV ES,AX **;**
; add your code here
**;the number of elements in LIST is SIZE (I think)
MOV CX,SIZE ;a loop counter, I think
;find the minimum value in LIST and store it into MINIMUM
;begin loop
AGAIN1:
LEA SI,LIST
LEA DI,MINIMUM
MOV AL,[SI]
CMP AL,[SI+1]
If carry flag=1:{I got no idea}
LOOP AGAIN1
;find the maximum value in LIST and store it into MAXIMUM
;Something similar to the other loop, but this time I gotta find the max.
AGAIN2:
LEA SI,LIST
LEA DI,MINIMUM
MOV AL,[SI]
CMP AL,[SI-1] ;???
LOOP AGAIN2
**
; exit to operating system.
MOV AX,4C00H
INT 21H
start endp
ends
end start ; set entry point and stop the assembler.
ret
I'm not positive, but I think you want to move the SIZE variable immediately after the LIST variable:
data segment
LIST DB 05H, 31H, 34H, 30H, 38H, 37H
SIZE=$-OFFSET LIST
MINIMUM DB ?
MAXIMUM DB ?
AVARAGE DB ?
ends
What it does is give you the number of bytes between the current address ($) and the beginning of the LIST variable - thus giving you the size (in bytes) of the list variable itself. Because the LIST is an array of bytes, SIZE will be the actual length of the array. If LIST was an array of WORDS, you'd have to divide SIZE by two. If your teacher wrote that code then perhaps you should leave it alone.
I'm not entirely clear on why your teacher made a stack segment, I can't think of any reason to use it, but perhaps it will become clear in a future assignment. For now, you probably should know that DUP is shorthand for duplicate. This line of code:
DW 128 DUP(0)
Is allocating 128 WORDS of memory initialized to 0.
The following lines of code:
MOV AX,DATA
MOV DS,AX
MOV ES,AX
Are setting up your pointers so that you can loop through the LIST. All you need to know is that, at this point, AX points to the beginning of the data segment and therefor the beginning of your LIST.
As for the rest... it looks like you have an endless loop. What you need to do is this:
Set SI to point to the beginning of the LIST
Set CX to be the length of the LIST, you've done that
Copy the first byte from [SI] to AX
Compare AX to the memory variable MINIMUM
If AX is smaller, copy it to MINIMUM
Increment IS and decriment CX
If CX = 0 (check the ZERO flag) exit the loop, otherwise, go back to #3