Mi instructor de clase de microprocesador nos asignó una tarea y dijo:
"Escribe un ensamblador en C." - Mi querido profesor
Entonces me pareció un poco ilógico.
Si no me equivoco, el lenguaje ensamblador es el primer paso del código de máquina al viaje de los lenguajes de nivel superior. Quiero decir que C es un lenguaje de nivel superior que el ensamblado. Entonces, ¿cuál es el punto de escribir un ensamblador en C? ¿Qué estaban haciendo en el pasado mientras la ausencia del lenguaje C? ¿Estaban escribiendo Assembler en Machine Code?
Para mí no tiene sentido escribir un traductor de código de máquina para un idioma de bajo nivel en un idioma de nivel superior.
Digamos que hemos creado una nueva arquitectura de microprocesador que ni siquiera hay un compilador de C para esa arquitectura. ¿Podrá nuestro ensamblador escrito en C simular la nueva arquitectura? Quiero decir, ¿será inútil o no?
Por cierto, sé que GNU Assembler y Netwide Assembler se han escrito en C. También me pregunto por qué están escritos en C.
Por último, este es el código fuente de ejemplo para un ensamblador simple que nuestro profesor nos dio:
// to compile, gcc assembler.c -o assembler
// No error check is provided.
// Variable names cannot start with 0-9.
// hexadecimals are twos complement.
// first address of the code section is zero, data section follows the code section.
//fout tables are formed: jump table, ldi table, label table and variable table.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
//Converts a hexadecimal string to integer.
int hex2int( char* hex)
{
int result=0;
while ((*hex)!='\0')
{
if (('0'<=(*hex))&&((*hex)<='9'))
result = result*16 + (*hex) -'0';
else if (('a'<=(*hex))&&((*hex)<='f'))
result = result*16 + (*hex) -'a'+10;
else if (('A'<=(*hex))&&((*hex)<='F'))
result = result*16 + (*hex) -'A'+10;
hex++;
}
return(result);
}
main()
{
FILE *fp;
char line[100];
char *token = NULL;
char *op1, *op2, *op3, *label;
char ch;
int chch;
int program[1000];
int counter=0; //holds the address of the machine code instruction
// A label is a symbol which mark a location in a program. In the example
// program above, the string "lpp", "loop" and "lp1" are labels.
struct label
{
int location;
char *label;
};
struct label labeltable[50]; //there can be 50 labels at most in our programs
int nooflabels = 0; //number of labels encountered during assembly.
// Jump instructions cannot be assembled readily because we may not know the value of
// the label when we encountered a jump instruction. This happens if the label used by
// that jump instruction appear below that jump instruction. This is the situation
// with the label "loop" in the example program above. Hence, the location of jump
// instructions must be stored.
struct jumpinstruction
{
int location;
char *label;
};
struct jumpinstruction jumptable[100]; //There can be at most 100 jumps
int noofjumps=0; //number of jumps encountered during assembly.
// The list of variables in .data section and their locations.
struct variable
{
int location;
char *name;
};
struct variable variabletable[50]; //There can be 50 varables at most.
int noofvariables = 0;
//Variables and labels are used by ldi instructions.
//The memory for the variables are traditionally allocated at the end of the code section.
//Hence their addresses are not known when we assemble a ldi instruction. Also, the value of
//a label may not be known when we encounter a ldi instruction which uses that label.
//Hence, the location of the ldi instructions must be kept, and these instructions must be
//modified when we discover the address of the label or variable that it uses.
struct ldiinstruction
{
int location;
char *name;
};
struct ldiinstruction lditable[100];
int noofldis=0;
fp = fopen("name_of_program","r");
if (fp != NULL)
{
while(fgets(line,sizeof line,fp)!= NULL) //skip till .code section
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".code")==0 )
break;
}
while(fgets(line,sizeof line,fp)!= NULL)
{
token=strtok(line,"\n\t\r "); //get the instruction mnemonic or label
//======================================== FIRST PASS ======================================================
while (token)
{
if (strcmp(token,"ldi")==0) //---------------LDI INSTRUCTION--------------------
{
op1 = strtok(NULL,"\n\t\r "); //get the 1st operand of ldi, which is the register that ldi loads
op2 = strtok(NULL,"\n\t\r "); //get the 2nd operand of ldi, which is the data that is to be loaded
program[counter]=0x1000+hex2int(op1); //generate the first 16-bit of the ldi instruction
counter++; //move to the second 16-bit of the ldi instruction
if ((op2[0]=='0')&&(op2[1]=='x')) //if the 2nd operand is twos complement hexadecimal
program[counter]=hex2int(op2+2)&0xffff; //convert it to integer and form the second 16-bit
else if (( (op2[0])=='-') || ((op2[0]>='0')&&(op2[0]<='9'))) //if the 2nd operand is decimal
program[counter]=atoi(op2)&0xffff; //convert it to integer and form the second 16-bit
else //if the second operand is not decimal or hexadecimal, it is a laber or a variable.
{ //in this case, the 2nd 16-bits of the ldi instruction cannot be generated.
lditable[noofldis].location = counter; //record the location of this 2nd 16-bit
op1=(char*)malloc(sizeof(op2)); //and the name of the label/variable that it must contain
strcpy(op1,op2); //in the lditable array.
lditable[noofldis].name = op1;
noofldis++;
}
counter++; //skip to the next memory location
}
else if (strcmp(token,"ld")==0) //------------LD INSTRUCTION---------------------
{
op1 = strtok(NULL,"\n\t\r "); //get the 1st operand of ld, which is the destination register
op2 = strtok(NULL,"\n\t\r "); //get the 2nd operand of ld, which is the source register
ch = (op1[0]-48)| ((op2[0]-48) << 3); //form bits 11-0 of machine code. 48 is ASCII value of '0'
program[counter]=0x2000+((ch)&0x00ff); //form the instruction and write it to memory
counter++; //skip to the next empty location in memory
}
else if (strcmp(token,"st")==0) //-------------ST INSTRUCTION--------------------
{
//to be added
}
else if (strcmp(token,"jz")==0) //------------- CONDITIONAL JUMP ------------------
{
//to be added
}
else if (strcmp(token,"jmp")==0) //-------------- JUMP -----------------------------
{
op1 = strtok(NULL,"\n\t\r "); //read the label
jumptable[noofjumps].location = counter; //write the jz instruction's location into the jumptable
op2=(char*)malloc(sizeof(op1)); //allocate space for the label
strcpy(op2,op1); //copy the label into the allocated space
jumptable[noofjumps].label=op2; //point to the label from the jumptable
noofjumps++; //skip to the next empty location in jumptable
program[counter]=0x5000; //write the incomplete instruction (just opcode) to memory
counter++; //skip to the next empty location in memory.
}
else if (strcmp(token,"add")==0) //----------------- ADD -------------------------------
{
op1 = strtok(NULL,"\n\t\r ");
op2 = strtok(NULL,"\n\t\r ");
op3 = strtok(NULL,"\n\t\r ");
chch = (op1[0]-48)| ((op2[0]-48)<<3)|((op3[0]-48)<<6);
program[counter]=0x7000+((chch)&0x00ff);
counter++;
}
else if (strcmp(token,"sub")==0)
{
//to be added
}
else if (strcmp(token,"and")==0)
{
//to be added
}
else if (strcmp(token,"or")==0)
{
//to be added
}
else if (strcmp(token,"xor")==0)
{
//to be added
}
else if (strcmp(token,"not")==0)
{
op1 = strtok(NULL,"\n\t\r ");
op2 = strtok(NULL,"\n\t\r ");
ch = (op1[0]-48)| ((op2[0]-48)<<3);
program[counter]=0x7500+((ch)&0x00ff);
counter++;
}
else if (strcmp(token,"mov")==0)
{
//to be added
}
else if (strcmp(token,"inc")==0)
{
op1 = strtok(NULL,"\n\t\r ");
ch = (op1[0]-48)| ((op1[0]-48)<<3);
program[counter]=0x7700+((ch)&0x00ff);
counter++;
}
else if (strcmp(token,"dec")==0)
{
//to be added
}
else //------WHAT IS ENCOUNTERED IS NOT AN INSTRUCTION BUT A LABEL. UPDATE THE LABEL TABLE--------
{
labeltable[nooflabels].location = counter; //buraya bir counter koy. error check
op1=(char*)malloc(sizeof(token));
strcpy(op1,token);
labeltable[nooflabels].label=op1;
nooflabels++;
}
token = strtok(NULL,",\n\t\r ");
}
}
//================================= SECOND PASS ==============================
//supply the address fields of the jump and jz instructions from the
int i,j;
for (i=0; i<noofjumps;i++) //for all jump/jz instructions
{
j=0;
while ( strcmp(jumptable[i].label , labeltable[j].label) != 0 ) //if the label for this jump/jz does not match with the
j++; // jth label in the labeltable, check the next label..
program[jumptable[i].location] +=(labeltable[j].location-jumptable[i].location-1)&0x0fff; //copy the jump address into memory.
}
// search for the start of the .data segment
rewind(fp);
while(fgets(line,sizeof line,fp)!= NULL) //skip till .data, if no .data, also ok.
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".data")==0 )
break;
}
// process the .data segment and generate the variabletable[] array.
int dataarea=0;
while(fgets(line,sizeof line,fp)!= NULL)
{
token=strtok(line,"\n\t\r ");
if (strcmp(token,".code")==0 ) //go till the .code segment
break;
else if (token[strlen(token)-1]==':')
{
token[strlen(token)-1]='\0'; //will not cause memory leak, as we do not do malloc
variabletable[noofvariables].location=counter+dataarea;
op1=(char*)malloc(sizeof(token));
strcpy(op1,token);
variabletable[noofvariables].name=op1;
token = strtok(NULL,",\n\t\r ");
if (token==NULL)
program[counter+dataarea]=0;
else if (strcmp(token, ".space")==0)
{
token=strtok(NULL,"\n\t\r ");
dataarea+=atoi(token);
}
else if((token[0]=='0')&&(token[1]=='x'))
program[counter+dataarea]=hex2int(token+2)&0xffff;
else if (( (token[0])=='-') || ('0'<=(token[0])&&(token[0]<='9')) )
program[counter+dataarea]=atoi(token)&0xffff;
noofvariables++;
dataarea++;
}
}
// supply the address fields for the ldi instructions from the variable table
for( i=0; i<noofldis;i++)
{
j=0;
while ((j<noofvariables)&&( strcmp( lditable[i].name , variabletable[j].name)!=0 ))
j++;
if (j<noofvariables)
program[lditable[i].location] = variabletable[j].location;
}
// supply the address fields for the ldi instructions from the label table
for( i=0; i<noofldis;i++)
{
j=0;
while ((j<nooflabels)&&( strcmp( lditable[i].name , labeltable[j].label)!=0 ))
j++;
if (j<nooflabels){
program[lditable[i].location] = (labeltable[j].location)&0x0fff;
printf("%d %d %d\n", i, j, (labeltable[j].location));
}
}
//display the resulting tables
printf("LABEL TABLE\n");
for (i=0;i<nooflabels;i++)
printf("%d %s\n", labeltable[i].location, labeltable[i].label);
printf("\n");
printf("JUMP TABLE\n");
for (i=0;i<noofjumps;i++)
printf("%d %s\n", jumptable[i].location, jumptable[i].label);
printf("\n");
printf("VARIABLE TABLE\n");
for (i=0;i<noofvariables;i++)
printf("%d %s\n", variabletable[i].location, variabletable[i].name);
printf("\n");
printf("LDI INSTRUCTIONS\n");
for (i=0;i<noofldis;i++)
printf("%d %s\n", lditable[i].location, lditable[i].name);
printf("\n");
fclose(fp);
fp = fopen("RAM","w");
fprintf(fp,"v2.0 raw\n");
for (i=0;i<counter+dataarea;i++)
fprintf(fp,"%04x\n",program[i]);
}
}
fuente
Respuestas:
La gente ha escrito ensambladores en código máquina. También escribieron luego en lenguaje ensamblador, a menudo un subconjunto del lenguaje que traducen ellos mismos, por lo que comienzan con una versión simple "bootstrap" del ensamblador, luego le agregan funciones cuando las necesitan para el ensamblador.
Sin embargo, nada de esto es particularmente una necesidad. Al final, un ensamblador es un programa de traducción (generalmente bastante) simple. Toma un archivo en un formato (texto) y escribe un archivo en otro (generalmente un formato de archivo de objeto).
El hecho de que el texto que se ingresa representa instrucciones de máquina en un formato de texto y el resultado representa las mismas instrucciones en formato binario no hace mucha diferencia al lenguaje que se usa para implementar el ensamblador; de hecho, incluso lenguajes más altos que C como SNOBOL y Python pueden funcionar bastante bien: recientemente trabajé (bastante) en un ensamblador escrito en Python, y funcionó bastante bien para el trabajo.
En cuanto a cómo arrancar las cosas inicialmente: generalmente en otra máquina que tenga herramientas de desarrollo decentes y tal. Si está desarrollando un nuevo hardware, generalmente comienza escribiendo un simulador (o al menos un emulador) para la nueva máquina de todos modos, por lo que al principio está construyendo y ejecutando el código en algún sistema host en cualquier caso.
fuente
Estás viendo conexiones que no existen.
"Escribir un ensamblador" es una tarea de programación como cualquier otra tarea de programación. Utiliza las herramientas para manejar esa tarea que son mejores para esa tarea. No hay nada especial en escribir un ensamblador; no hay ninguna razón para no escribirlo en un lenguaje de alto nivel. C está en realidad en un nivel bastante bajo, y probablemente preferiría C ++ o algún otro lenguaje de nivel superior.
El lenguaje ensamblador es en realidad totalmente inadecuado para una tarea como esta. Los casos en los que usaría razonablemente el lenguaje ensamblador son muy, muy raros. Solo cuando necesita hacer cosas que no se pueden expresar en un lenguaje de nivel superior.
fuente
El ensamblaje es esencialmente un mnemónico para el código de máquina; cada código de operación en el lenguaje de máquina recibe un mnemónico de ensamblaje, es decir, en x86 NOP es 0x90. Esto hace que el ensamblador sea bastante simple (la mayoría de los ensambladores tienen dos pases, uno para traducir y otro para generar / resolver direcciones / referencias). El primer ensamblador fue escrito y traducido a mano (probablemente en papel) al código de la máquina. Una versión mejor está escrita y ensamblada con el ensamblador 'ensamblado' a mano, las nuevas características se agregan de esta manera. Los compiladores para nuevos idiomas se pueden construir de esta manera; ¡en el pasado era común que los compiladores produjeran ensamblajes y usaran un ensamblador para su back-end!
Si bien el autohospedaje es un hito común / característica deseable para un lenguaje de programación, el ensamblaje es de un nivel tan bajo que la mayoría de los programadores preferirían trabajar en un nivel superior. Es decir, nadie quiere escribir un ensamblador en ensamblaje (o cualquier otra cosa realmente)
Bootstrapping es el proceso de obtener una cadena de herramientas en una nueva arquitectura.
El proceso básico es:
No es necesario escribir una vez en ensamblador (nuevo o antiguo) para hacer esto, uno debe elegir el mejor idioma para escribir su ensamblador / back-end / generador de código.
¡Los ensambladores no simulan!
Si uno estaba desarrollando una nueva CPU con un lenguaje de máquina nuevo (o existente), generalmente es necesario un simulador para las pruebas; es decir, ejecute instrucciones y datos aleatorios a través del simulador, y compare la salida con las mismas instrucciones y datos en su CPU prototipo. Luego encuentre errores, corrija errores, repita.
fuente
Entre las razones para escribir un ensamblador en C (o cualquier otro lenguaje de nivel superior) se encuentran todas las razones que puede usar para justificar la escritura de cualquier otro programa en ese lenguaje de nivel superior. Los principales en este caso son probablemente la portabilidad y la usabilidad.
Portabilidad: si escribe su ensamblador en un idioma nativo, tiene un ensamblador en esa plataforma. Si lo escribe en C, tiene un ensamblador en cualquier plataforma con un compilador de C. Esto le permite, por ejemplo, compilar código para su plataforma integrada en su estación de trabajo y mover el archivo binario en lugar de tener que hacerlo todo directamente en el dispositivo de destino.
Usabilidad: para la mayoría de las personas, leer, razonar y modificar programas es mucho más natural cuando el programa está en un lenguaje de nivel superior que cuando está en ensamblador o (peor) código máquina sin procesar. Por lo tanto, es más fácil desarrollar y mantener el ensamblador en un lenguaje de nivel superior porque puedes pensar en términos de las abstracciones que te brindan los idiomas de nivel superior en lugar de tener que pensar en las minucias de las que eres responsable en los idiomas inferiores.
fuente
Abordar específicamente esta parte de la pregunta solamente:
"Por cierto, sé que GNU Assembler y Netwide Assembler han sido escritos en C. También me pregunto por qué están escritos en C".
Hablando como parte del equipo que originalmente escribió el Netwide Assembler, la decisión nos pareció tan obvia en ese momento que básicamente no consideramos ninguna otra opción, pero si lo hubiéramos hecho, habríamos llegado a la misma conclusión, según las siguientes razones:
Esto hizo que la decisión fuera bastante fácil: C en cumplimiento de ANSI (también conocido como C89 en estos días) era el único lenguaje en el momento que realmente alcanzó todos esos puntos. Si hubiera habido un C ++ estandarizado en ese entonces, podríamos haberlo considerado, pero el soporte de C ++ entre diferentes sistemas era bastante irregular en ese entonces, por lo que escribir C ++ portátil fue una pesadilla.
fuente
Una cosa no tiene absolutamente nada que ver con la otra. ¿Los navegadores web deben escribirse estrictamente con html o php u otro lenguaje de contenido web? No, ¿por qué lo harían? ¿Los automóviles solo pueden ser conducidos por otros automóviles y no por humanos?
Convertir un blob de bits (algunos ascii) en otro blob de bits (algún código de máquina) es solo una tarea de programación, el lenguaje de programación que utiliza para esa tarea es el que desee. Puede y ha habido ensambladores escritos en muchos idiomas diferentes.
Inicialmente, los nuevos idiomas no se pueden escribir en su propio idioma ya que todavía no hay un compilador / ensamblador para ellos. Si no hay un compilador existente para un nuevo idioma, tiene que escribir el primero en otro idioma y, finalmente, puede iniciar si eso tiene sentido. (html y un navegador web, un programa que toma algunos bits y escupe algunos bits nunca se escribirá en html, no puede ser).
No tiene que ser un idioma nuevo, puede ser uno existente. Los nuevos compiladores de C o C ++ no se compilan automáticamente desde el principio.
Para lenguaje ensamblador y C, los dos primeros idiomas para casi todos los conjuntos de instrucciones nuevos o modificados. No estamos en el pasado, estamos en el presente. Podemos generar fácilmente un ensamblador en C o Java o Python o lo que sea para cualquier conjunto de instrucciones y lenguaje de ensamblaje que queramos, incluso si aún no existe. Del mismo modo, hay muchos compiladores de C que se pueden volver a armar que podemos generar en lenguaje ensamblador para cualquier lenguaje ensamblador que queramos, incluso si el ensamblador aún no existe.
Eso es exactamente lo que hacemos con un nuevo conjunto de instrucciones. Tome alguna computadora que no se ejecute en nuestro nuevo conjunto de instrucciones con su compilador C que se compiló no para nuestro nuevo conjunto de instrucciones ni para su ensamblador, cree un ensamblador cruzado y un compilador cruzado. Desarrolle y use eso mientras crea y simula la lógica. Realice los ciclos de desarrollo normales para encontrar un error, corregir un error y probar nuevamente, hasta que idealmente todas las herramientas y la lógica se consideren listas. Y dependiendo del objetivo, digamos que es un microcontrolador incapaz de ejecutar un sistema operativo, nunca tendría una razón para arrancar de tal manera que la cadena de herramientas genere y se ejecute utilizando el conjunto de instrucciones nativo. Siempre cruzarías la compilación. Aparte de en una máquina wayback, nunca tiene sentido escribir el ensamblador en ensamblador.
Sí, si pudieras regresar o fingir regresar, el primer ensamblador era un humano con un lápiz y papel, que escribía algo que tenía sentido para ellos y luego escribía los bits al lado que tenían sentido para la lógica. Luego usé interruptores o alguna otra forma de introducir los bits en la máquina (google pdp8 o pdp11 o altair 8800) y hacer que haga algo. Inicialmente, no había simuladores de computadora, solo tenía que tener la lógica correcta mirándolo lo suficiente o también girando varias revoluciones del chip. Hoy en día, las herramientas son lo suficientemente buenas como para que puedas obtener el éxito A0 en el sentido de que la cosa es más que una gran resistencia, muchas de ellas funcionan, es posible que aún necesites un giro para cosas que no podrías simular por completo, pero a menudo puedes arrancar ahora en el primer spi sin tener que esperar al tercer o cuarto giro,
En su máquina wayback como era de esperar, entonces toma su código ensamblado a mano y lo usa para decir cargar un programa de cinta o tarjetas. También puede codificar manualmente un ensamblador en código de máquina, puede que no sea completo, pero que hace que la programación sea un poco más fácil. Luego, esa herramienta se usa para crear una que pueda manejar un lenguaje más avanzado o complicado (un ensamblador de macros), y esa para crear una más complicada y terminas con FORTRAN o BASIC o B o lo que sea. Y luego comienzas a pensar en bootstrapping en el mismo idioma, reescribiendo el compilador cruzado para que sea un compilador nativo. por supuesto, idealmente necesita un entorno o sistema operativo de algún tipo para eso.
Cuando estamos creando o probando silicio, podemos / debemos mirar las señales que son unos y ceros. Las herramientas nos mostrarán binario o hexadecimal por defecto y es posible con algunas herramientas incluso tener búsquedas para que las herramientas muestren algunos mnemotécnicos (ensamblaje quizás) pero a menudo los ingenieros (silicio / hardware y software) pueden leer suficiente el código de máquina, o use el desmontaje / listado para "ver" las instrucciones
Dependiendo de lo que esté haciendo, podría insertar un poco de código de máquina en los vectores de prueba en lugar de volver a escribir y recompilar o volver a ensamblar la prueba. Por ejemplo, si tiene una tubería y una captación previa a cierta profundidad, es posible que necesite o desee completar más allá del final del programa una cantidad de nops u otras instrucciones reales para que la tubería no vomite en instrucciones indefinidas, y puede elegir simplemente complete el código de la máquina en el archivo / listado de nivel más bajo en lugar de intentar que el compilador o ensamblador o enlazador lo haga.
Al probar el procesador, por supuesto, debe lidiar con indefinidos y tal vez no le interesen los bits, etc. Por lo tanto, deberá ingresar al código de la máquina y modificar uno o más bits específicos en una instrucción en un programa que normalmente funciona. ¿Vale la pena escribir un programa para hacer esto o simplemente hacerlo a mano? Del mismo modo, al probar ECC, desea voltear uno o más bits y ver si se corrigen o quedan atrapados. De acuerdo, es mucho más fácil escribir un programa para hacer o puedes hacerlo a mano.
Luego, por supuesto, hay idiomas que no producen código que se ejecuta en un procesador, pascal temprano, java, python, etc. Necesita una máquina virtual escrita en otro idioma solo para usar esos idiomas. No puede usar su compilador de Java para hacer un Java VM, no tiene sentido según el diseño del lenguaje.
(sí, claro, después de la implementación pura de estos lenguajes, eventualmente alguien construye un backend impuro que a veces puede apuntar a conjuntos de instrucciones reales, no al conjunto de instrucciones vm y luego, en ese caso, puede usar el lenguaje para compilarlo él mismo o su vm si realmente sintió el necesidad. Un front end de gnu java a gcc por ejemplo).
Con el tiempo y probablemente aún no escribimos compiladores de C en C. Usamos cosas como bison / flex algún otro lenguaje de programación que usamos para generar el C para nosotros que no queríamos escribir nosotros mismos. Algún porcentaje está en C seguro, pero otro porcentaje está en algún otro lenguaje que usa algún otro compilador que ingresa bits y genera otros bits. A veces, este enfoque también se utiliza para generar un ensamblador. Hasta el diseñador del compilador / ensamblador (programas que tienen la tarea de ingresar bits y luego emitir otros bits) en cuanto a cómo lo van a implementar. Los analizadores generados por el programa podrían programarse a mano con seguridad, lo que lleva mucho tiempo para que la gente busque un atajo. Al igual que podría escribir un ensamblador en ensamblador, pero la gente busca un atajo.
Un navegador web es solo un programa que toma algunos bits y escupe algunos otros bits. Un ensamblador es solo un programa que toma algunos bits y escupe algunos otros bits. Un compilador es solo un programa que toma algunos bits y escupe algunos otros bits. Etc. Para todos estos hay un conjunto documentado de reglas para los bits de entrada y de salida para cada tarea de programación. Estas tareas y bits son lo suficientemente genéricos como para que se pueda utilizar cualquier lenguaje de programación DISPONIBLE (que sea capaz de manipular bit / byte y manejar las entradas y salidas). La clave aquí está disponible. Obtenga y pruebe el libro / tutorial de Linux desde cero. Pruebe un pdp8 o pdp11 o altair 8800 u otro simulador con un panel frontal simulado.
fuente