Previous chapter Index Appendix I

Chapter 8

Machine level operations

Computer magazines and other such publications often print details of 'Machine code programs' and 'Patches' that you can make in order to give your computer system special facilities. For example, you may see a short machine code program that someone has written that will let you 'read' the date and time recorded by the clock built into your computer, or you might see details of a Patch that will let you modify the characters displayed on the screen.

The design of machine code programs and patches requires a lot of detailed technical knowledge about your computer and about the software you are using on it. Such technical detail is outside the scope of this manual: for that sort of information you will need to turn to a book on Machine-code or Assembler programming on the microprocessor in your computer.

However, making use of published programs and patches is really very straightforward and that is what we describe in this chapter.

8.1 General information

All the software to run your computer is held in your computer's memory, as an array of individual codes. Each code takes up one byte of computer memory and has its own 'address' so it can be readily found.

When you switch on your computer and load the operating system, all the routines that make up the operating system are read into one area of the computer's memory - usually at the 'top' of the memory (high-numbered addresses). Similarly, when you load BASIC, the software that makes up the BASIC programming language is read into a separate area of memory. Any BASIC program you want to load and the variables this program generates are held in a third area, which BASIC automatically reserves as its working area.

'Patches' either change a piece of information or how a particular routine works, for example causing the program to 'jump to a particular routine. To implement a patch, you have to write new codes at the locations in memory of the information you want to change. The result of this patch will appear automatically whenever the affected code is accessed.

Machine code programs usually contain extra routines that you might want to make use of and to make such a routine available, you generally have to write the codes that make up the routine into an empty area of memory. Normally the easiest place to put these codes is at the top of the area BASIC reserves as its working area (your programs rarely need to use all of this area). Having stored the routine, you then have to arrange that the piece of memory you have used is no longer treated as part of BASIC's working area. (This is in fact essential if you will be calling the routine from BASIC.) To use the routine, you have to specifically direct your computer to execute the codes of the additional routine: to do this, you need to remember the address of the first code in the routine because theis is the entry point into the program.

So to use published patches and machine-code programs, you need to be able to:

Commands that do each of these actions are available within BASIC.

8.2 Applying a Patch

The BASIC command used to patch bytes in memory is the POKE command, POKE address, new-code. This writes the new code at the address given in the command, replacing whatever code was there before. So you just need a separate POKE command for each byte you want to change.

The address needed by the POKE is the location of the byte: this can be any integer between 0 and 65535. The new code is also expressed as an integer, and the code occupies one byte, this can take any value between 0 and 255.

The addresses and the new codes form the details of the patch; they will probably be written either as decimal numbers or as hexadecimal numbers. Either form is equally acceptable in the POKE command. You just have to copy these details into POKE commands - indeed, they may even be already presented as POKE commands. These POKE commands can then either form a short BASIC 'Set-up' program or be included in another BASIC program.

The following short program illustrates such a short 'Set-up' program. This is in fact the preparation program that users of Amstrad PCW computers need to run before using the routines of GSX, the Graphics Extension to CP/M. It sets the 'jump' to GSX.


100 GSX%=&H30

110 POKE GSX%+0, &H50 'LD D,B

120 POKE GSX%+1, &H59 'LD E,C

130 POKE GSX%+2, &HE 'LD C, 115

140 POKE GSX%+3, 115

150 POKE GSX%+4, &HC3 'JP &H0005

160 POKE GSX%+5, &H5

170 POKE GSX%+6, &H0

8.3 Loading a machine code program

Loading a machine code program is very like applying a patch, but with a few extra steps that reserve the area of memory for it. The stages involved are therefore:

Allocating the memory

The amount of memory required for the routine is defined by the number of codes in the routine. For example the following routine (which will fetch the time when you are running Mallard BASIC on your Spectrum) needs 42 bytes to store it:


&HE5, &HD5, &HC5, &H21, &H00, &H00, &HE5, &HE5

&H39, &HEB, &H0E, &H69, &HCD, &H05, &H00, &HD1

&HD1, &H06, &H03, &H6F, &HE6, &HF0, &H67, &HAD

&HCB, &H3C, &H84, &HCB, &H3C, &HCB, &H3C, &H84

&HE1, &H77, &H23, &H36, &H00, &H7A, &H53, &H10

&HEA, &HC9

This memory then needs to be taken off the top of BASIC's working area. To do this, you use two more of BASIC's commands and functions - the HIMEM function to tell you the current address of the top byte in this area of memory, and the MEMORY command to move the top of the area to another address. In fact, you will typically put these together into the one instruction MEMORY HIMEM-bytes; so for example, to divide off a section of memory for the time routine given above, you would use:


   MEMORY HIMEM-42

As the whole point of loading an external routine is to use it, the area used to store an external routine isn't re-absorbed into BASIC's working area at the end of the program. However, this means that any program that carves off a section of memory from the working area should only be run once per session - or, if it does need to be run again, the corresponding MEMORY HIMEM+bytes should be used first. Otherwise, the working area will be gradually whittled away because the HIMEM isn't reset to its original value until you load BASIC again.

Note: When you have more than one external routine or function you want to use, you need to divide off an area from BASIC's working area for each routine. This can be done either in one program or in a number of different programs - for example, one program for each routine. Each of these programs would have its own MEMORY instruction. Of course, the order in which you execute these programs will affect wheter each routine is actually stored in memory and so it is a good idea to keep careful track of this.

Loading the routine

The process of loading the routine simply involves executing a series of POKE commands, which write the individual codes of the routine into memory - starting at the byte immediately above the new HIMEM.

Because the routine is to be inserted into consecutive bytes, these POKE commands can readily be handled by a FOR...NEXT loop. For example, the following could be used to load the above Time routine from a number of DATA statements:


100 FOR i = 1 TO 42

110 READ j

120 POKE HIMEM + i, j

130 NEXT

200 DATA &HE5, &HD5, &HC5, &H21, &H00, &H00, &HE5, &HE5

210 DATA &H39, &HEB, &H0E, &H69, &HCD, &H05, &H00, &HD1

220 DATA &HD1, &H06, &H03, &H6F, &HE6, &HF0, &H67, &HAD

230 DATA &HCB, &H3C, &H84, &HCB, &H3C, &HCB, &H3C, &H84

240 DATA &HE1, &H77, &H23, &H36, &H00, &H7A, &H53, &H10

250 DATA &HEA, &HC9

As well as listing the codes, the designer will normally give the total sum of the codes because this provides a simple check of whether you have typed all the codes into your program correctly. This total is known as the checksum.

The checksum for the time routine is 5177. You could put this into your routine as follows:


90  jsum = 0

100 FOR i = 1 TO 42

110 READ j : jsum=jsum+j

120 POKE HIMEM + i, j

130 NEXT

140 IF jsum<>5177 THEN PRINT "Error in DATA": STOP

200 DATA &HE5, &HD5, &HC5, &H21, &H00, &H00, &HE5, &HE5

210 DATA &H39, &HEB, &H0E, &H69, &HCD, &H05, &H00, &HD1

220 DATA &HD1, &H06, &H03, &H6F, &HE6, &HF0, &H67, &HAD

230 DATA &HCB, &H3C, &H84, &HCB, &H3C, &HCB, &H3C, &H84

240 DATA &HE1, &H77, &H23, &H36, &H00, &H7A, &H53, &H10

250 DATA &HEA, &HC9

8.4 Using a machine code program

The way you use a machine code program that you've loaded into memory depends on whether it forms a routine (a package of instructions) or a function (a packaged expression).

Using an external routine

To use an external routine, you CALL it, giving the address of the entry point to the routine and the names of any variables you need to pass between the routine and the rest of your program.

The entry point to the routine is usually the address of its first byte, ie. HIMEM+1. To pass this address to the CALL command, you first record this value as a variable and then use this variable in the CALL instruction. For example, you might record the entry point to the time routine as get.time=HIMEM+1.

The variables you pass are defined by the routine itself. For example, the time routine requires three integer variables, into which it will put the hour, the minute and the second. These variables can then be used in the rest of your program: for example, you might decide simply to print them out as follows:


  300 get.time=HIMEM+1

  310 CALL get.time(hour%,min%,secs%)

  320 PRINT "Time is now ";USING "##:##:##"; hour%,min%,secs%

Using an external function

To use an external function, you first have to define it but then you can use it just like any other function.

The process of defining it associates the function with one of the 10 names reserved for external functions, USR0...USR9. The command used to do this is DEF USR, and again you need to give the entry point to the routine (the address of the first byte).

Suppose the machine code program you had just poked into memory formed an external function. Then the following instructions could be used to define it as the external function USR0:


  300 address=HIMEM+1

  310 DEF USR0=address

To use this function, you might then have some instruction like:

  320 result=USR0(value)

where result is the result of the function USR0 on value.

8.5 Examples

The following are examples of BASIC programs which use the techniques described in this chapter. Specifically, they show how you can these techniques to fetch the current date and time by running a Mallard BASIC program under CP/M on your Spectrum.

The first part of each program sets up a machine code routine to call the operating system, asking for details of the date or the time as appropriate: the second part of the program shows the way in which the machine-code routine should be used. The hexadecimal numbers in the DATA statements are the machine code of the routines themselves.

The machine code routines are stored in some of the memory which was initially allocated to BASIC (reserved by the MEMORY HIMEM-bytes statements).

The programs are designed to be run just once per session. If you need to run them again, remember to return the area of memory reserved for the machine code to BASIC first, otherwise the memory will be gradually whittled away.

The programs simply use the routines once and print the results. Clearly there are many more useful things that you might do, for example with extra CALLs to the machine code routine. Of course, CALLs must be in exactly the form given, with one DATE or three TIME parameters - all integers.

Example 1: Getting the date from CP/M


100 REM

110 REM Fetching the date

120 REM

130 REM Lines 190..260 Set up the machine code required and

140 REM                 should only be executed once

150 REM Lines 280..500 Give an example of use

160 REM  NB: The number returned is the count of days with

170 REM  Day 1 being Jan 1st 1978

180 REM

190 MEMORY HIMEM - 20

200 jsum = 0

210 FOR i = 1 TO 20 : READ j : jsum = jsum + j : POKE HIMEM + i, j : NEXT

220 IF jsum <>2465 THEN PRINT "Error in DATA statement" : STOP

230 get.date = HIMEM + 1

240 DATA &HE5, &H21, &H00, &H00, &HE5, &HE5, &H39, &HEB

250 DATA &H0E, &H69, &HCD, &H05, &H00, &HD1, &HE1, &HE1

260 DATA &H73, &H23, &H72, &HC9

270 REM

280 CALL get.date(day%)

290 REM

300 day% = day% + 365 + 366 - 1 : weekday% = day% MOD 7

310 IF weekday% = 0 THEN weekday$ = "Thursday"

320 IF weekday% = 1 THEN weekday$ = "Friday"

330 IF weekday% = 2 THEN weekday$ = "Saturday"

340 IF weekday% = 3 THEN weekday$ = "Sunday"

350 IF weekday% = 4 THEN weekday$ = "Monday"

360 IF weekday% = 5 THEN weekday$ = "Tuesday"

370 IF weekday% = 6 THEN weekday$ = "Wednesday"

380 year% = 1976 + 4 * (day% \ (365+365+365+366))

390 day% = day% MOD (365+365+365+366)

400 IF day% = (31+29-1) THEN day% = 29 : month$ = "FEB" : GOTO 470

410 IF day% > (31+29-1) THEN day% = day% - 1

420 year% = year% + day% \ 365

430 day% = day% MOD 365

440 READ month.days%, month$

460 IF day% >= month.days% THEN day% = day% - month.dayS% : GOTO 440

470 PRINT USING"& & ##/&/####";"Today is";weekday$,day%,month$,year%

480 END

490 DATA 31, JAN, 28, FEB, 31, MAR, 30, APR, 31, MAY, 30, JUN

500 DATA 31, JUL, 31, AUG, 30, SEP, 31, OCT, 30, NOV, 31, DEC

Example 2: Getting the time from CP/M


100 REM

110 REM Fetching the time

120 REM

130 REM Lines 210..310 Set up the machine code required and

140 REM                 should only be executed once

150 REM Lines 330..360 Give an example of use

160 REM  NB: You must give all parameters in the order given

170 REM  even if you only wish to use some of them. You can

180 REM  change their names if you wish, but they must all be

190 REM  integer variables.

200 REM

210 jsum = 0

220 MEMORY HIMEM-42

230 FOR i = 1 TO 42

240 READ j : jsum=jsum+j

250 POKE HIMEM + i, j

260 NEXT

270 IF jsum<>5177 THEN PRINT "Error in DATA": STOP

280 DATA &HE5, &HD5, &HC5, &H21, &H00, &H00, &HE5, &HE5

290 DATA &H39, &HEB, &H0E, &H69, &HCD, &H05, &H00, &HD1

300 DATA &HD1, &H06, &H03, &H6F, &HE6, &HF0, &H67, &HAD

310 DATA &HCB, &H3C, &H84, &HCB, &H3C, &HCB, &H3C, &H84

320 DATA &HE1, &H77, &H23, &H36, &H00, &H7A, &H53, &H10

330 DATA &HEA, &HC9

320 REM

330 WIDTH 255

340 CALL get.time(hour%,mins%,secs%)

350 PRINT USING "The time is now ##:##:## &";hour%;mins%,secs%;CHR$(13);

360 GOTO 340


Previous chapter Index Appendix I