Programación en X68000 capítulo 6, programas TRS
Introducción.
Human68K es un sistema operativo como MS-DOS y como tal no tiene multi taréa. ¿O si que la tiene?
Human68k nos provee con la capacidad de hacer cosas similares a la multi tarea.
Podemos construir un programa TSR que significa Terminate and Stay Resident o Terminar y quedarse residente.
Un programa TSR es uno que comienza, establece ciertos parámetros y termina. Pero en vez de irse de la memoria, se queda.
La tarea de establecer parámetros es normalmente instalar una interrupción que core una sub rutina o función.
Podemos hacer cosas como ejecutar la función cuando se dispare la interrupción de presionar ciertas teclas del teclado o cuando se de un evento desde la impresora.
Human68k tiene un lugar en la memoria con un vector que apunta a las diferentes rutinas del sistema cuando ciertas interrupciones ocurren.
Un programa no necesita ser hecho residente para sacar partido de estas interrupciones, de hecho la mayoría de video juegos las utilizan para controlar cosas como el refresco vertical y horizontal de pantalla de manera que se puedan hacer efectos gráfico o para sincronizar con el fotograma.
Pero los programas TSR solo son útiles usando estas interrupciones.
Una vez un programa residente esta en memoria y se ejecuta cuando es disparado por una interrupción, todavía podemos usar el sistema operativo como si fuese multi tarea.
En este artículo voy a mostrar las instrucciones, las estructuras de datos, como hacer un programa residente y un mecanismo para comunicar con el a través de la línea de comando.
Las instrucciones:
Las instrucciones clave para hacerlo posible son las siguientes:
struct _psp* _dos_getpdb() //get the Program Segment Prefix void* _dos_intvcs(int intno, void* jobadr) //Set vector handling address void _dos_keeppr (int prglen, int code) //Terminate and reside
Por el camino veremos unas cuantas instrucciones mas pero estas tres son las importantes.
Iremos siguiento el codigo fuente que puedes encontrar en:
https://github.com/FedericoTech/X68KTutorials/blob/main/blogresi/main.cEl programa en memoria:
Primero neceistamos entender como los programas son almancenados en memoria.
Aquí hay un diagrama que muestra como se almacenan los programas en memorya y las funciones para manipular los diferentes aspectos.
Cuando ejecutamos un programa o se queda residente, el sistema operativo hace seguimiento de el con dos estructuras de datos. Una estructura es struct _psp que significa Program Segmen Prefix (PSP) que significa Prefijo segmento de programa. Lo de prefijo porque va antes del programa. Y la otra es struct _mep que significa Memory Entry Pointer (MEP) o puntero a entrada de memoria.
Estructura de datos Program Segment Prefix.
El Program Segment Prefix es una estructura de datos donde los sistemas operativos como MS-DOS hace seguimiento de los diferentes parámetros del programa.
Parámetros tales como punteros a la cadena de la línea de comandos, variables de entorno, el nombre de fichero, la ruta, bits de estado del sistema operativo, la memoria dinámica, el puntero la pila del usuario, el puntero la pila del supervisor, punteros a las rutinas de salida, cancelación y error, etc.
La declaración de la estructura Program SegmentPrefix es como esto:
struct _psp {
char *env; // Pointer to environment variables.
void *exit; // Pointer to the Exit routine.
void *ctrlc; // Pointer to the CTRL+C routine
void *errexit; // Pointer to the Error routine.
char *comline; // Pointer to the command line data structure
unsigned char handle[12];
void *bss; // Pointer to the Block Started by Symbol
void *heap; // Pointer to the heap
void *stack; // Pointer to the stack
void *usp; // User Stack Pointer
void *ssp; // Supervisor Stack Pointer
unsigned short sr; // Status Register
unsigned short abort_sr; // backup of Status Register when interrupted (perhaps)
void *abort_ssp; // Supervisor Stack Pointer backup when interrupted (perhaps)
void *trap10;
void *trap11;
void *trap12;
void *trap13;
void *trap14;
unsigned int_ osflg; // OS flags
unsigned char reserve_1[28]; // (1.b module number, 3.b unused, 1.l Memory management pointer for loaded child process, 5.l unused)
char exe_path[68]; // path to the exec
char exe_name[24]; // name of the exec
char reserve_2[36];
};
Hay varias maneras de obtener un puntero a esta estructura de datos.
La función struct _psp* _dos_getpdb();
Algunos compuladores proveen de pinteros a la misma:
La dirección extern int _PSP; en formáto entero.
El la dirección extern struct _psp *_procp; en formato de puntero.
La estructura de datos Memory Entry Pointer (MEP).
El Memory Entry Pointer o MEP es una lista de doble enlace que relaciona el programa con otros programas en memoria. Se coloca justo antes del PSP y tiene este aspecto:
struct _mep {
void *prev_mp;
void *parent_mp;
void *block_end;
void *next_mp;
};
El strct _mep tiene 4 punteros y es 16 bytes de largo. 4 bytes por puntero.
El primer campo void *prev_mp apunta al struct _mep del programa anterior.
El segundo campo void *parent_mp apunta al struct _mep del programa que inicio este programa. Normalmente Command.com
El tercer campo void *block_end apunta al último byte del programa en memoria.
El cuarto campo void *next_mp apunta al struct _mep del siguiente programa.
Hay varias formas de obtener un puntero a la estructura de datos MEP.
La primera y mas común es obtener a dirección al PSP con la función struct _psp* _dos_getpdb(); y restar el tamaño de MEP que son 16 bytes.struct _mep* mep = (struct _mep*)((int) _dos_getpdb() - sizeof(struct _mep));
o combinaciones con el puntero a PSP si estan disponibles con el compilador.
struct _mep* mep = (struct _mep*) (_PSP - sizeof(struct _mep)); struct _mep* mep = (struct _mep*) ((int) _psp - sizeof(struct _mep));
Algunos compiladores dan acceso directo al puntero:
extern struct _mep *_memcp; // direct access to it
Algunos compiladores también dan acceso directo a void *block_end con un puntero o un entero.
extern void *_last; // pointer format extern int _HEND; //integer format
El programa
El programa se sitúa justo después del PSP y el void *block_end.
Encontrar el programa residente.
Nuestro programa tendrá dos parte, la parte residente y la parte no residente.
La parte residente is solo una función que ejecuta una tarea cuando es disparada por una interrupción.
La parte no residente nos da una interfaz para poner y quitar la parte residente.
Cuando la parte no residente se inicia, explorará la lista de programas para averiguar si la parte residente ya estaba instalada de modo que se pueda quitar con seguridad.
Al explorar la lista de programas vamos a buscar una secuencia de bytes, una palabra, que el programa residente tendrá justo después de las estructuras MEP y PSP y al principio del programa de manera que podamos encontrarlo.
En este ejemplo usaré struct _psp* _dos_getpdb(); en vez de los punteros porque podrían no estar disponibles en todos los compiladores.
#ifdef __MARIKO_CC__
#include
#include
#else
#include
#include
//substitutions for Lyndux toolchain
#define interrupt __attribute__ ((interrupt_handler))
#define _mep dos_mep
#define _psp dos_psp
// _dos_getpdb() is broken
extern int _PSP;
#define _dos_getpdb() (void*)_PSP
#endif
int main(int argc, char *argv[])
{
struct _psp* psp = _dos_getpdb();
struct _mep* mep = (struct _mep*)((int) psp - sizeof(struct _mep));
void* start_program = (void *)((int) psp + sizeof(struct _psp));
void* end_address = mep->block_end;
}
Al ejecutar la parte no residente (el que ocupa la línea de comando), está garantizado que es el último programa en la lista de modo que tenemos que buscar los programas que hay detras de el. La dirección del programa anterior está en el campo prev_mp. Este es también la dirección de su estructura MEP.
Como pertenece al própio programa podemos manipularlo sin privilegios de supervisor de modo que podemos almacenarlo tal cual en una variable entera:
resident_start_addr = (unsigned int) mep-> prev_mp; Alternativamente, como la dirección es el primer campo de la estructura podríamos hacer: resident_start_addr = (unsigned int) mep;
Ahora necesitamos recorrer la lista con un bucle hasta que resident_start_addr sea 0, pero como estaremos leyendo áreas de memoria fuera de nuestro programa, necesitamos o activar los privilegios de supervisor con int _dos_super(long stack); o usar funciones que nos permitan obtener el contenido de esas direcciones.
Usar int _dos_super(long stack); es mas sencillo pero en este ejemplo he decidido usar las otras funciones de modo que las conozcamos.
Este es el aspecto del bucle:
while(resident_start_addr != 0){
// we read a long word with _iocs_b_lpeek so that we don't need to go Super
resident_start_addr = _iocs_b_lpeek((void *) resident_start_addr);
// check something and exit the loop
}
Aquí usamos la función int _iocs_b_lpeek (const void *addr); para coger un entero de tamaño (4 bytes) desde la dirección dada.
Ya somos capaces de explorar la lista de programas pero necesitamos encontrar algo que identifique el programa residente.
Antes mencionaba que en este ejemplo la estrategia es colocar algo en la memoria del programa que podamos encontrar relativo al programa que lo identifique si está presente.
Esta es una solución de muy de bajo nivel que encontré en un programa de ensamblador y que he conseguido imitar en C.
Como hemos visto hasta ahora, podemos obtener la estructura de datos de PSP. Por delante esta la estructura de datos MEP y por detrás está el programa.
Podemos escribir alguna información justo al principio del programa de modo que podamos encontrarla mas tarde.
Esta información sera una variable de tipo cadena de C cuyo espacio será puesto en código ensamblador.
Para que funcione, si el programa que estamos construyendo tiene mas ficheros objeto, el fichero donde esta la función main tiene que ser el primero en los comandos de compilado y enlazado. De otra manera seremos capaces de hacer el programa residente pero no de encontrarlo en la memoria.
Otra consideración es que el código ensamblador tiene que estar delante de cualquier linealización o implementación.
Podemos hacer declaración anticipada o declarar una variable pero sin inicializar la porque de hacerlo perdemos el sitio para el ensamblado.
El código es el siguiente:
extern char keyword[4]; // declaration only
asm(" _keyword: .ds.b 4 "); //this is the actual storage.
int found = 0; //flag in case we find the resident program.
int main(int argc, char *argv[])
{
unsigned int resident_start_addr;
struct _mep* mep = (struct _mep*)((int) _dos_getpdb() - sizeof(struct _mep));
resident_start_addr = (unsigned int) mep-> prev_mp;
Como habrás notado, el nombre de la variable en ensamblador es _keyword: mientras que la variable en C no esta precedida por la línea baja.
Si hemos seguido los pasos correctamente, tendremos la variable keyword almacenada justo al principio del programa de manera que siempre podremos encontrarla.
La variable keyword es para que podamos poner la palabra de identificación antes de hacer el programa residente.
La palabra de identificativa debería ser una palabra que no pueda ser confundida con una instrucción de ensamblador de manera que esté garantizado que no encontraremos la misma combinación de bytes al principio de otros programas y evitar que la CPU intente ejecutarla.
En este ejemplo “MMV” es una palabra segura ya que no hay tal combinación de bytes en el juego de instrucciones de M68000.
De vuelta al bucle ahora podemos buscar la palabra:
extern char keyword[4]; // declaration only
asm(" _keyword: .ds.b 4 "); //this is the actual storage.
int found = 0; //flag in case we find the resident program.
int main(int argc, char *argv[])
{
unsigned int resident_start_addr;
struct _mep* mep = (struct _mep*)((int) _dos_getpdb() - sizeof(struct _mep));
resident_start_addr = (unsigned int) mep-> prev_mp;
while(resident_start_addr != 0){
char keyword[4] = {0}; // we will copy the first 4 bytes found in other programs in this var.
// we read a long word with _iocs_b_lpeek so that we don't need to go Super
resident_start_addr = _iocs_b_lpeek((void *) resident_start_addr);
//we capture the address of the beginning of the program
void * beginning_of_program = (void *)resident_start_addr + sizeof(struct _mep) + sizeof(struct _psp);
//we copy three byte in our buffer. we use _iocs_b_memstr so that we don't need to go Super
_iocs_b_memstr(
beginning_of_program,
keyword,
3
);
//if we find the process
if(strncmp(“MMV”, keyword, 3) == 0){
found = 1;
break;
}
}
El algoritmo de arriba calcula la dirección donde empieza el programa sumando los tamaños de MEP y PSP después copia 3 bytes en el buffer keyword. Después lo comparamos con la palabra MMV y si la encontramos, lo señalamos y salimos del bucle.
Según parece, para acceder ciertos datos necesitamos estar calculando diferencias de un lado para otro todo el rato, obteniendo la dirección de PSP, después restando el tamaño de MEP para hallar su dirección o sumar el tamaño de PSP para obtener la dirección donde empieza el programa para así alcanzar la palabra clave. Sin embargo hay una forma mas directa de hacer esto.
Podemos usar esta estructura:
struct resident {
struct {
struct _mep mep; //16 bytes
struct _psp psp; //240 bytes
} procc; //256 bytes
char keyword[4]; //here the program starts
}
La primera vez aún necesitamos obtener el puntero a PSP y restar el tamaño de MEP (16 bytes). Pero si convertimos la dirección de MEP en (struct resident *), ahora podemos acceder a todos los campos directamente, y dentro del bucle donde exploramos la lista de programas también podemos convertir la dirección en este tipo.
Hagamos los cambios:
struct resident {
struct {
struct _mep mep; //16 bytes
struct _psp psp; //240 bytes
} procc; //256 bytes
char keyword[4]; //here the program starts
};
extern char keyword[4];
asm(" _keyword: .ds.b 4 "); //this is the actual storage.
int found = 0; //flag in case we find the resident program.
int main(int argc, char *argv[])
{
unsigned int resident_start_addr;
struct resident * _resident, * resident_aux;
struct _psp* psp = _dos_getpdb();
_resident = (struct resident *)((unsigned int) psp - sizeof(struct _mep));
resident_start_addr = (unsigned int) _resident->procc.mep.prev_mp;
while(resident_start_addr != 0){
char keyword[4] = {0}; // we will copy the first 4 bytes found in other programs in this var.
void* beginning_of_program;
// we read a long word with _iocs_b_lpeek so that we don't need to go Super
resident_start_addr = _iocs_b_lpeek((void *) resident_start_addr);
//we capture the address of the beginning of the program
resident_aux = (struct resident *) resident_start_addr;
//we copy three byte in our buffer. we use _iocs_b_memstr so that we don't need to go Super
_iocs_b_memstr(
resident_aux->keyword,
keyword,
3
);
//if we find the process
if(strncmp("MMV", keyword, 3) == 0){
found = 1;
break;
}
}
if(found) {
// what is next?
}
};
La tarea.
El propósito de hacer un programa residente es ejecutar una tarea de fondo.
Necesitamos implementar una función. Esta función no es una función cualquiera sino que tiene que ser una interrupción.
Si el compilador provee de la librería, podemos hacer #include <interrupt.h> y delcarar esta función:
#include <interrupt.h> void interrupt process_start();
Si el compilador no tiene la librería interrupt.h aún podemos declararla de esta manera:
void __attribute__ ((interrupt_handler)) process_start();
Esta función tiene que ser disparada por alguna interrupción del sistema.
Podría ser que la interrupción es disparada muy poco, solo cuando pulsamos ciertas teclas, o muy frecuentemente como cuando se produce el escaneo vertical de la pantalla 60 veces por segundo o incluso mas rápido, cada mili-segundo.
Si la función tarda demasiado en ejecutar, podría ocurrir que la propia interrupción la interrumpe y empieza una nueva ejecución y se entre en una condición de bloqueo y finalmente el sistema se caiga. Esto es una condición de carrera.
Para prevenir esto podemos tener un indicador que actúe de mutex (exclusión mútua) de modo que si una nueva llamada a la interrupción encuentra el indicador en 0, significa que una ejecución previa esta aún en curso y esta nueva instancia termina ahí y la ejecución previa puede continuar desde el punto donde fue interrumpida.
El indicador de exclusión mutua tiene que ser volatile de modo que le indiquemos al compilador que cambio en esta variable tienen efectos colaterales y que no la intente optimizar.
La tarea en este ejemplo es un contador que se muestra en la esquina superior izquierda de la pantalla, de color amarillo y que se actualiza cada segundo.
int counter;
int volatile mutex = 1;
//the resident program is this interrupt
void interrupt process_start() //it has to be an interrupt
{
//we check whether the interrupt is still being processed
if(mutex){
mutex = 0; //we hold the mutex
//only do things every 100 cycles
if(++counter % 100 == 0){
//we get the current position of the cursor
int cursor_pos = _iocs_b_locate(-1, -1);
//we get the current colour.
char previous_color = _iocs_b_color(-1);
//we move the cursor to the top left corner
_iocs_b_locate(0, 0);
//we change the colour
_iocs_b_color(2 + 4 + 8);
printf("Count %d\n", counter);
//we re establish cursor's position
_iocs_b_locate((cursor_pos >> 16) & 0xffff, cursor_pos & 0xffff);
_iocs_b_color(previous_color);
}
mutex = 1;
}
}
Hacer el programa residente.
Ahora tenemos forma de encontrar una instancia residente de nuestro programa y una tarea que ejecutar de fondo pero no lo hemos hecho residente aún.
Necesitamos establecer la interrupción con la instrucción void* _dos_intvcs(int intno, void* jobadr) y salir del programa con void _dos_keeppr (int prglen, int code).
La instrucción void* _dos_intvcs(int intno, void* jobadr) toma el número de interrupción del vector de interrupciones, y la dirección a de nuestra tarea. Como retorno nos da la dirección que tenia esa interrupción.
En esta URL puedes encontrar al final, la tabla de interrupciones y escoger la que quieras usar.
https://www.chibiakumas.com/68000/x68000.php
En mi ejemplo uso la interrupción 0x45, UserInterrupt: MFP Timer-C (Mouse/cursor/FDD control, etc.) que se dispara 100 por segundo.
Cuando se instala una interrupción necesitamos guardar la dirección que se nos devuelve para poder restaurarla cuando la parte residente, la que está de fondo, termine.
En mi ejemplo almaceno la dirección retornada en el puntero oldvector el cual también uso como indicador en lugar de la variable found.
No es necesario hacerlo de esta manera, puedes usar una variable normal pero en mi ejemplo uso otra variable declarada en ensamblador después de la variable keyword. La incluiré por tanto en la estructura struct resident.
La instrucción void _dos_keeppr (int prglen, int code) toma el tamaño del programa como primer parámetro y el código de salida como segundo.
El tamaño del programa es desde el final de PSP hasta la dirección a la que apunta el campo void *block_end en MEP.
Hay varias maneras de calcular el tamaño:
struct _psp* psp = _dos_getpdb(); struct _mep* mep = (struct _mep*)((unsigned int) psp - sizeof(struct _mep)); unsigned int start_program_adders = (unsigned int) psp + sizeof(struct _psp); _dos_keeppr((unsigned int) mep-> block_end - start_program_adders, 0);
Si el compilador lo permite también se puede hacer esto:
unsigned int start_program_adders = _PSP + sizeof(struct _psp); _dos_keeppr(_HEND – start_program_adders, 0);
Para hacer el programa residente, por tanto, hacemos esta secuencia de instrucciones:
unsigned int resident_addr, end_addr;
//the program starts right where the keyword is.
resident_addr = (unsigned int) &_resident->keyword;
//where is where the program ends
end_addr = (unsigned int) _resident->procc.mep.block_end;
//we set the keyword
_iocs_b_memstr(
KEYWORDS,
&keyword,
3
);
//we get the current vector address
oldvector = _dos_intvcs(TIMER_C, process_start);
_dos_c_print("resident program started\r\n");
//we make the program resident
_dos_keeppr(
end_addr - resident_addr, //memory from beginning of the program
0 //exit code.
);
Si sin embargo el programa ya tenia la parte residente en memoria, vamos a restaurar la interrupción original, detenerlo y borrarlo.
El código para eso es este:
// we remove it
_dos_intvcs(TIMER_C, oldvector); //we reestablish 0x45
_dos_mfree(&resident_aux->procc.psp); //psp_addr
_dos_c_print("resident program stopped\r\n");
El programa final luce de esta manera:
#ifdef __MARIKO_CC__
#include <doslib.h>
#include <iocslib.h>
#else
#include <dos.h>
#include <iocs.h>
#include <stdio.h>
//substitutions for Lyndux toolchain
#define interrupt __attribute__ ((interrupt_handler))
#define _mep dos_mep
#define _psp dos_psp
#define oldvector _oldvector
// _dos_getpdb() is broken
extern int _PSP;
#define _dos_getpdb() (void*)_PSP
#endif
#define TIMER_C 0x45
#define KEYWORDS "MMV"
struct resident {
struct {
struct _mep mep; //16 bytes
struct _psp psp; //240 bytes
} procc; //256 bytes
char keyword[4]; //here the program starts
void * oldvector;
};
extern char keyword[4];
extern void * oldvector;
void interrupt process_start(); //forward declaration valid before the assembly.
//these have to be the very first thing
asm( " _keyword: .ds.b 4 " ); // zero initialized 4 byte storage for keyword
asm( " _oldvector: .dc.l 0 " ); // oldvector address also initialized by 0
int main(int argc, char *argv[])
{
struct resident * _resident, * resident_aux;
int resident_start_addr;
struct _psp* psp = _dos_getpdb();
_resident = (struct resident *)((unsigned int) psp - sizeof(struct _mep));
resident_start_addr = (unsigned int) _resident->procc.mep.prev_mp;
while(resident_start_addr != 0){
char keyword[4] = {0}; // we will copy the first 4 bytes found in other programs in this var.
void* beginning_of_program;
// we read a long word with _iocs_b_lpeek so that we don't need to go Super
resident_start_addr = _iocs_b_lpeek((void *) resident_start_addr);
//we capture the address of the beginning of the program
resident_aux = (struct resident *) resident_start_addr;
//we copy three byte in our buffer. we use _iocs_b_memstr so that we don't need to go Super
_iocs_b_memstr(
resident_aux->keyword,
keyword,
3
);
//if we find the process
if(strncmp(KEYWORDS, keyword, 3) == 0){
//we copy the current value of the offset 4 from the beginning of the program which is the oldvector declared in assembly.
oldvector = (void *) _iocs_b_lpeek(&resident_aux->oldvector);
break;
}
}
// if found...
if(oldvector) {
// we remove it
_dos_intvcs(TIMER_C, oldvector); //we reestablish 0x45
_dos_mfree(&resident_aux->procc.psp); //psp_addr
_dos_c_print("resident program stopped\r\n");
//if not found
} else {
unsigned int resident_addr, end_addr;
//the program starts right where the keyword is.
resident_addr = (unsigned int) &_resident->keyword;
//where is where the program ends
end_addr = (unsigned int) _resident->procc.mep.block_end;
//we set the keyword
_iocs_b_memstr(
KEYWORDS,
&keyword,
3
);
//we get the current vector address
oldvector = _dos_intvcs(TIMER_C, process_start);
_dos_c_print("resident program started\r\n");
//we make the program resident
_dos_keeppr(
end_addr - resident_addr, //memory from beginning of the program
0 //exit code.
);
}
}
int counter;
int volatile mutex = 1;
//the resident program is this interrupt
void interrupt process_start() //it has to be an interrupt
{
//we check whether the interrupt is still being processed
if(mutex){
mutex = 0; //we hold the mutex
//only do things every 200 cycles
if(++counter % 100 == 0){
//we get the current position of the cursor
int cursor_pos = _iocs_b_locate(-1, -1);
//we get the current colour.
char previous_color = _iocs_b_color(-1);
//we move the cursor to the top left corner
_iocs_b_locate(0, 0);
//we change the colour
_iocs_b_color(2 + 4 + 8);
printf("Count %d\n", counter);
//we re establish cursor's position
_iocs_b_locate((cursor_pos >> 16) & 0xffff, cursor_pos & 0xffff);
//we re establish text colour
_iocs_b_color(previous_color);
}
mutex = 1;
}
}
Entonces, ahora si llamamos lanzamos el ejecutable por primera vez, instalara el contador en la pantalla y contará mientras que nosotros aun seremos capaces de hacer otras cosas. Y si lo lanzamos por segunda vez, va a buscar la instancia del la parte residente y lo detendrá.
Espero que disfrutes de este tutorial.

Comentarios