General > QB64 Discussion
QB64, Assembly, and Software Bloat
fatman2021:
Calling SDL from assembly on AROS:
--- Code: ---BITS 32
GLOBAL main
EXTERN SDL_Init
EXTERN SDL_Quit
EXTERN SDL_SetVideoMode
SDL_INIT_VIDEO EQU 0x20
SECTION .text
main:
push dword SDL_INIT_VIDEO
call SDL_Init
add esp, 4
push dword 0
push dword 32
push dword 480
push dword 640
call SDL_SetVideoMode
add esp, 16
call SDL_Quit
ret
--- End code ---
chronokitsune:
--- Quote from: Johny B. on April 17, 2012, 08:28:36 AM ---Oh, and by the way: QuickBasic did NOT include any of the interpreter components in the final executable. bc.exe and was used to compile code, while qb.exe provided an IDE and a interpreter. In fact, now that I think about it, I can't recall any type of BASIC that made executables by bundling the source and interpreter together.
--- End quote ---
QB4.5 allowed you to create a standalone EXE, but that wasn't the default selected, mainly because the most useful features such as COMMON and CHAIN stuff wasn't preserved whereas using BRUN45 (BASIC runtime version 4.5) preserved such information.
fatman2021:
The if..then statement is just a special case of the if..then..else statement (with an empty ELSE block). Therefore, we'll only consider the more general if..then..else form. The basic implementation of an if..then..else statement in 80x86 assembly language looks something like this
--- Code: --- {Sequence of statements to test some condition
Jcc ElseCode
{Sequence of statements corresponding to the THEN block}
jmp EndOfIF
ElseCode:
{Sequence of statements corresponding to the ELSE block}
EndOfIF:
--- End code ---
Note: Jcc represents some conditional jump instruction.
For example, to convert the Pascal statement:
--- Code: ---IF (a=b) then c := d else b := b + 1;
--- End code ---
to assembly language, you could use the following 80x86 code:
--- Code: --- mov ax, a
cmp ax, b
jne ElseBlk
mov ax, d
mov c, ax
jmp EndOfIf
ElseBlk:
inc b
EndOfIf:
--- End code ---
For simple expressions like (A=B) generating the proper code for an if..then..else statement is almost trivial. Should the expression become more complex, the associated assembly language code complexity increases as well. Consider the following if statement presented earlier:
--- Code: --- IF ((X > Y) and (Z < T)) or (A<>B) THEN C := D;
--- End code ---
When processing complex if statements such as this one, you'll find the conversion task easier if you break this if statement into a sequence of three different if statements as follows:
--- Code: --- IF (A<>B) THEN C := D
IF (X > Y) THEN IF (Z < T) THEN C := D;
--- End code ---
This conversion comes from the following Pascal equivalences:
--- Code: ---IF (expr1 AND expr2) THEN stmt;
--- End code ---
is equivalent to
--- Code: ---IF (expr1) THEN IF (expr2) THEN stmt;
--- End code ---
and
--- Code: ---IF (expr1 OR expr2) THEN stmt;
--- End code ---
is equivalent to
--- Code: ---IF (expr1) THEN stmt;
IF (expr2) THEN stmt;
--- End code ---
In assembly language, the former if statement becomes:
--- Code: --- mov ax, A
cmp ax, B
jne DoIF
mov ax, X
cmp ax, Y
jng EndOfIf
mov ax, Z
cmp ax, T
jnl EndOfIf
DoIf:
mov ax, D
mov C, ax
EndOfIF:
--- End code ---
As you can probably tell, the code necessary to test a condition can easily become more complex than the statements appearing in the else and then blocks. Although it seems somewhat paradoxical that it may take more effort to test a condition than to act upon the results of that condition, it happens all the time. Therefore, you should be prepared for this situation.
Probably the biggest problem with the implementation of complex conditional statements in assembly language is trying to figure out what you've done after you've written the code. Probably the biggest advantage high level languages offer over assembly language is that expressions are much easier to read and comprehend in a high level language. The HLL version is self-documenting whereas assembly language tends to hide the true nature of the code. Therefore, well-written comments are an essential ingredient to assembly language implementations of if..then..else statements. An elegant implementation of the example above is:
--- Code: ---; IF ((X > Y) AND (Z < T)) OR (A <> B) THEN C := D;
; Implemented as:
; IF (A <> B) THEN GOTO DoIF;
mov ax, A
cmp ax, B
jne DoIF
; IF NOT (X > Y) THEN GOTO EndOfIF;
mov ax, X
cmp ax, Y
jng EndOfIf
; IF NOT (Z < T) THEN GOTO EndOfIF ;
mov ax, Z
cmp ax, T
jnl EndOfIf
; THEN Block:
DoIf: mov ax, D
mov C, ax
; End of IF statement
EndOfIF:
--- End code ---
Admittedly, this appears to be going overboard for such a simple example. The following would probably suffice:
--- Code: ---; IF ((X > Y) AND (Z < T)) OR (A <> B) THEN C := D;
; Test the boolean expression:
mov ax, A
cmp ax, B
jne DoIF
mov ax, X
cmp ax, Y
jng EndOfIf
mov ax, Z
cmp ax, T
jnl EndOfIf
; THEN Block:
DoIf: mov ax, D
mov C, ax
; End of IF statement
EndOfIF:
--- End code ---
However, as your if statements become complex, the density (and quality) of your comments become more and more important.
fatman2021:
The Pascal case statement takes the following form :
--- Code: --- CASE variable OF
const1:stmt1;
const2:stmt2;
.
.
.
constn:stmtn
END;
--- End code ---
When this statement executes, it checks the value of variable against the constants const1 ... constn. If a match is found then the corresponding statement executes. Standard Pascal places a few restrictions on the case statement. First, if the value of variable isn't in the list of constants, the result of the case statement is undefined. Second, all the constants appearing as case labels must be unique. The reason for these restrictions will become clear in a moment.
Most introductory programming texts introduce the case statement by explaining it as a sequence of if..then..else statements. They might claim that the following two pieces of
Pascal code are equivalent:
--- Code: --- CASE I OF
0: WriteLn('I=0');
1: WriteLn('I=1');
2: WriteLn('I=2');
END;
IF I = 0 THEN WriteLn('I=0')
ELSE IF I = 1 THEN WriteLn('I=1')
ELSE IF I = 2 THEN WriteLn('I=2');
--- End code ---
While semantically these two code segments may be the same, their implementation is usually different. Whereas the if..then..else if chain does a comparison for each conditional statement in the sequence, the case statement normally uses an indirect jump to transfer control to any one of several statements with a single computation. Consider the two examples presented above, they could be written in assembly language with the following code:
--- Code: --- mov bx, I
shl bx, 1 ;Multiply BX by two
jmp cs:JmpTbl[bx]
JmpTbl word stmt0, stmt1, stmt2
Stmt0: print
byte "I=0",cr,lf,0
jmp EndCase
Stmt1: print
byte "I=1",cr,lf,0
jmp EndCase
Stmt2: print
byte "I=2",cr,lf,0
EndCase:
; IF..THEN..ELSE form:
mov ax, I
cmp ax, 0
jne Not0
print
byte "I=0",cr,lf,0
jmp EndOfIF
Not0: cmp ax, 1
jne Not1
print
byte "I=1",cr,lf,0
jmp EndOfIF
Not1: cmp ax, 2
jne EndOfIF
Print
byte "I=2",cr,lf,0
EndOfIF:
--- End code ---
Two things should become readily apparent: the more (consecutive) cases you have, the more efficient the jump table implementation becomes (both in terms of space and speed). Except for trivial cases, the case statement is almost always faster and usually by a large margin. As long as the case labels are consecutive values, the case statement version is usually smaller as well.
What happens if you need to include non-consecutive case labels or you cannot be sure that the case variable doesn't go out of range? Many Pascals have extended the definition of the case statement to include an otherwise clause. Such a case statement takes the following form:
--- Code: --- CASE variable OF
const:stmt;
const:stmt;
. .
. .
. .
const:stmt;
OTHERWISE stmt
END;
--- End code ---
If the value of variable matches one of the constants making up the case labels, then the associated statement executes. If the variable's value doesn't match any of the case labels, then the statement following the otherwise clause executes. The otherwise clause is implemented in two phases. First, you must choose the minimum and maximum values that appear in a case statement. In the following case statement, the smallest case label is five, the largest is 15:
--- Code: --- CASE I OF
5:stmt1;
8:stmt2;
10:stmt3;
12:stmt4;
15:stmt5;
OTHERWISE stmt6
END;
--- End code ---
Before executing the jump through the jump table, the 80x86 implementation of this case statement should check the case variable to make sure it's in the range 5..15. If not, control should be immediately transferred to stmt6:
--- Code: --- mov bx, I
cmp bx, 5
jl Otherwise
cmp bx, 15
jg Otherwise
shl bx, 1
jmp cs:JmpTbl-10[bx]
--- End code ---
The only problem with this form of the case statement as it now stands is that it doesn't properly handle the situation where I is equal to 6, 7, 9, 11, 13, or 14. Rather than sticking extra code in front of the conditional jump, you can stick extra entries in the jump table as follows:
--- Code: --- mov bx, I
cmp bx, 5
jl Otherwise
cmp bx, 15
jg Otherwise
shl bx, 1
jmp cs:JmpTbl-10[bx]
Otherwise: {put stmt6 here}
jmp CaseDone
JmpTbl word stmt1, Otherwise, Otherwise, stmt2, Otherwise
word stmt3, Otherwise, stmt4, Otherwise, Otherwise
word stmt5
etc.
--- End code ---
Note that the value 10 is subtracted from the address of the jump table. The first entry in the table is always at offset zero while the smallest value used to index into the table is five (which is multiplied by two to produce 10). The entries for 6, 7, 9, 11, 13, and 14 all point at the code for the Otherwise clause, so if I contains one of these values, the Otherwise clause will be executed.
There is a problem with this implementation of the case statement. If the case labels contain non-consecutive entries that are widely spaced, the following case statement would generate an extremely large code file:
--- Code: --- CASE I OF
0: stmt1;
100: stmt2;
1000: stmt3;
10000: stmt4;
Otherwise stmt5
END;
--- End code ---
In this situation, your program will be much smaller if you implement the case statement with a sequence of if statements rather than using a jump statement. However, keep one thing in mind- the size of the jump table does not normally affect the execution speed of the program. If the jump table contains two entries or two thousand, the case statement will execute the multi-way branch in a constant amount of time. The if statement implementation requires a linearly increasing amount of time for each case label appearing in the case statement.
Probably the biggest advantage to using assembly language over a HLL like Pascal is that you get to choose the actual implementation. In some instances you can implement a case statement as a sequence ofif..then..else statements, or you can implement it as a jump table, or you can use a hybrid of the two:
--- Code: --- CASE I OF
0:stmt1;
1:stmt2;
2:stmt3;
100:stmt4;
Otherwise stmt5
END;
--- End code ---
could become:
--- Code: --- mov bx, I
cmp bx, 100
je Is100
cmp bx, 2
ja Otherwise
shl bx, 1
jmp cs:JmpTbl[bx]
etc.
--- End code ---
Of course, you could do this in Pascal with the following code:
--- Code: --- IF I = 100 then stmt4
ELSE CASE I OF
0:stmt1;
1:stmt2;
2:stmt3;
Otherwise stmt5
END;
--- End code ---
But this tends to destroy the readability of the Pascal program. On the other hand, the extra code to test for 100 in the assembly language code doesn't adversely affect the readability of the program (perhaps because it's so hard to read already). Therefore, most people will add the extra code to make their program more efficient.
The C/C++ switch statement is very similar to the Pascal case statement. There is only one major semantic difference: the programmer must explicitly place a break statement in each case clause to transfer control to the first statement beyond the switch. This break corresponds to the jmp instruction at the end of each case sequence in the assembly code above. If the corresponding break is not present, C/C++ transfers control into the code of the following case. This is equivalent to leaving off the jmp at the end of the case's sequence:
--- Code: --- switch (i)
{
case 0: stmt1;
case 1: stmt2;
case 2: stmt3;
break;
case 3: stmt4;
break;
default:stmt5;
}
--- End code ---
This translates into the following 80x86 code:
--- Code: --- mov bx, i
cmp bx, 3
ja DefaultCase
shl bx, 1
jmp cs:JmpTbl[bx]
JmpTbl word case0, case1, case2, case3
case0: <stmt1's code>
case1: <stmt2's code>
case2: <stmt3's code>
jmp EndCase ;Emitted for the break stmt.
case3: <stmt4's code>
jmp EndCase ;Emitted for the break stmt.
DefaultCase: <stmt5's code>
EndCase:
--- End code ---
fatman2021:
Demonstrating the use of Lib-C in 32-bit x86 assembly:
--- Code: ---; Source: powers.asm - Determine if a number is a power of two.
;
; Author: Joey DeFrancesco
;
; Build:
; nasm -felf -g -F stabs powers.asm
; gcc powers.o -o powers
;
BITS 32
[Section .data] ; initialized data
PromptMsg: db "Enter digit: ", 0x00
InPrompt: db '%d', 0x00
OutFmtString: db "%d is a power of two!",0x0A, 0x00
FailString: db "Sorry, %d is not a power of two.",0x0A, 0x00
[Section .bss] ; uninitialized data
IntVal resd 1 ; reserve double word for input by user
[Section .text] ; code
; importing function from glibc
extern printf
extern puts
extern scanf
global main ; entry point
main:
nop ; to keep gdb happy
; prolog (C calling convention)
push ebp
mov ebp, esp
push ebx
push esi
push edi
; printf() - output string
push PromptMsg
call printf
add esp, 4 ; clean stack, one arg
; scanf() - get input from user
push IntVal
push InPrompt
call scanf
add esp, 8 ; Initially wasn't here but should have been!
; Determine if number is indeed a power of two.
; (x AND (x-1)) == 0, then x is power of two else it is not
mov eax, [IntVal]
dec eax
and dword eax, [IntVal]
cmp eax, 0
jz .Success
.Fail: ; not a power of two
; printf()
push dword [IntVal]
push FailString
call printf
add esp, 4
jmp .Exit
.Success: ; input was a power of two
; printf() - display success string
push dword [IntVal]
push OutFmtString
call printf
add esp, 4
.Exit:
; Epilog (C calling convention)
pop edi
pop esi
pop ebx
mov esp, ebp
pop ebp
ret ; return control to linux
; End
--- End code ---
Navigation
[0] Message Index
[#] Next page
[*] Previous page
Go to full version