Anteriormente tenia aqui una pagina que no termine por falta de tiempo sobre compiladores, decidi reemplazarla por una más completa sobre un tema relacionado; veremos como implementa un compilador (GCC) algunas estrucutas fundamentales y como lo podemos ver en su forma "cruda" o "raw", empezaremos con la estructura básica de una función:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat -n main.c
1 int main()
2 {
3 }
Cuando programamos nunca pensamos en como el compilador crea el archivo objeto, damos por hecho las cosas y nunca nos sentamos a pensar en como las implementariamos si quisieramos programar un lenguaje o algo similar:
Veamos entonces como se implementan las funciones en lenguaje C ( compilador GCC, aunque la mayoria es igual ) Veamos el binario raw del archivo main.o:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.o | od -x
offset | ( cada columna tiene 2 bytes )
----------------------------------------------
0000000 457f 464c 0101 0001 0000 0000 0000 0000 |cada numero se guarda(ceros tambien)
0000020 0001 0003 0001 0000 0000 0000 0000 0000 |cada numero ocupa 1 nibble
0000040 00b4 0000 0000 0000 0034 0000 0000 0028 |1 nubble son 4 bits
0000060 0009 0006 8955 83e5 08ec e483 b8f0 0000 |2 nibbles =1 bytes = 8 bits
0000100 0000 c429 c3c9 0000 4700 4343 203a 4728 |2 bytes =16 bits
0000120 554e 2029 2e33 2e33 2035 4428 6265 6169
0000140 206e 3a31 2e33 2e33 2d35 3331 0029 2e00
0000160 7973 746d 6261 2e00 7473 7472 6261 2e00
0000200 6873 7473 7472 6261 2e00 6574 7478 2e00
0000220 6164 6174 2e00 7362 0073 6e2e 746f 2e65
0000240 4e47 2d55 7473 6361 006b 632e 6d6f 656d
0000260 746e 0000 0000 0000 0000 0000 0000 0000
0000300 0000 0000 0000 0000 0000 0000 0000 0000
0000320 0000 0000 0000 0000 0000 0000 001b 0000
0000340 0001 0000 0006 0000 0000 0000 0034 0000
0000360 0012 0000 0000 0000 0000 0000 0004 0000
0000400 0000 0000 0021 0000 0001 0000 0003 0000
0000420 0000 0000 0048 0000 0000 0000 0000 0000
0000440 0000 0000 0004 0000 0000 0000 0027 0000
0000460 0008 0000 0003 0000 0000 0000 0048 0000
0000500 0000 0000 0000 0000 0000 0000 0004 0000
0000520 0000 0000 002c 0000 0001 0000 0000 0000
0000540 0000 0000 0048 0000 0000 0000 0000 0000
0000560 0000 0000 0001 0000 0000 0000 003c 0000
0000600 0001 0000 0000 0000 0000 0000 0048 0000
0000620 0026 0000 0000 0000 0000 0000 0001 0000
0000640 0000 0000 0011 0000 0003 0000 0000 0000
0000660 0000 0000 006e 0000 0045 0000 0000 0000
0000700 0000 0000 0001 0000 0000 0000 0001 0000
0000720 0002 0000 0000 0000 0000 0000 021c 0000
0000740 0080 0000 0008 0000 0007 0000 0004 0000
0000760 0010 0000 0009 0000 0003 0000 0000 0000
0001000 0000 0000 029c 0000 000d 0000 0000 0000
0001020 0000 0000 0001 0000 0000 0000 0000 0000
0001040 0000 0000 0000 0000 0000 0000 0001 0000
0001060 0000 0000 0000 0000 0004 fff1 0000 0000
0001100 0000 0000 0000 0000 0003 0001 0000 0000
0001120 0000 0000 0000 0000 0003 0002 0000 0000
0001140 0000 0000 0000 0000 0003 0003 0000 0000
0001160 0000 0000 0000 0000 0003 0004 0000 0000
0001200 0000 0000 0000 0000 0003 0005 0008 0000
0001220 0000 0000 0012 0000 0012 0001 6d00 6961
0001240 2e6e 0063 616d 6e69 0000
0001251
De lo anterior podemos ver que existe mucha información solo para crear una función, cosa que no creo, entonces que es tanta información si solo defini una función ? Veamos que pasa si sacamos los strings del archivo:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.o | strings
GCC: (GNU) 3.3.5 (Debian 1:3.3.5-13)
.symtab
.strtab
.shstrtab
.text
.data
.bss
.note.GNU-stack
.comment
main.c
main
Ahh, entonces el compilador tambien escribe los nombres de sección, versión del compilador, nombre de funciones y del archivo. Veamos el mismo archivo pero ahora visto como instrucciones en ensamblador, lo veremos mediante un desensamblador del paquete nasm:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm -b 32 main.o
Offset opcode instruccion en ensamblador
------------------------------------
00000000 7F45 jg 0x47
00000002 4C dec esp
00000003 46 inc esi
00000004 0101 add [ecx],eax
00000006 0100 add [eax],eax
00000008 0000 add [eax],al
0000000A 0000 add [eax],al
0000000C 0000 add [eax],al
0000000E 0000 add [eax],al
00000010 0100 add [eax],eax
00000012 0300 add eax,[eax]
00000014 0100 add [eax],eax
00000016 0000 add [eax],al
00000018 0000 add [eax],al
0000001A 0000 add [eax],al
0000001C 0000 add [eax],al
0000001E 0000 add [eax],al
00000020 B400 mov ah,0x0
00000022 0000 add [eax],al
00000024 0000 add [eax],al
00000026 0000 add [eax],al
00000028 3400 xor al,0x0
0000002A 0000 add [eax],al
0000002C 0000 add [eax],al
0000002E 2800 sub [eax],al
00000030 0900 or [eax],eax
00000032 06 push es
00000033 005589 add [ebp-0x77],dl
00000036 E583 in eax,0x83
00000038 EC in al,dx
00000039 0883E4F0B800 or [ebx+0xb8f0e4],al
0000003F 0000 add [eax],al
00000041 0029 add [ecx],ch
00000043 C4 db 0xC4
00000044 C9 leave
00000045 C3 ret
00000046 0000 add [eax],al
00000048 004743 add [edi+0x43],al
0000004B 43 inc ebx
0000004C 3A20 cmp ah,[eax]
0000004E 28474E sub [edi+0x4e],al
00000051 55 push ebp
00000052 2920 sub [eax],esp
00000054 332E xor ebp,[esi]
00000056 332E xor ebp,[esi]
00000058 3520284465 xor eax,0x65442820
0000005D 626961 bound ebp,[ecx+0x61]
00000060 6E outsb
00000061 2031 and [ecx],dh
00000063 3A33 cmp dh,[ebx]
00000065 2E332E xor ebp,[cs:esi]
00000068 352D313329 xor eax,0x2933312d
......mas codigo
0000009D 7465 jz 0x104
0000009F 2E47 cs inc edi
000000A1 4E dec esi
000000A2 55 push ebp
000000A3 2D73746163 sub eax,0x63617473
000000A8 6B002E imul eax,[eax],byte +0x2e
000000AB 636F6D arpl [edi+0x6d],bp
000000AE 6D insd
000000AF 656E gs outsb
000000B1 7400 jz 0xb3
00000299 0001 add [ecx],al
.......lo mismo
0000029B 0000 add [eax],al
0000029D 6D insd
0000029E 61 popa
0000029F 696E2E63006D61 imul ebp,[esi+0x2e],dword 0x616d0063
000002A6 69 db 0x69
000002A7 6E outsb
000002A8 00 db 0x00
Vemos que un archivo objeto tiene más información que el propio código ejecutable. Para poder ver el unicamente código binario generado por las instrucciones que escribimos ( en este caso int main(){} ) podemos hacerlo de varias formas, una es con objdump ).
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objdump -d main.o
main.o: file format elf32-i386
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 83 e4 f0 and $0xfffffff0,%esp
9: b8 00 00 00 00 mov $0x0,%eax
e: 29 c4 sub %eax,%esp
10: c9 leave
11: c3 ret
-----------------------------------------------------
offset | opcode |instruccion en asm
en byte| |
========================================================
Vea que la seccion .comment contiene el nombre de la
version de compilador GCC,
( en la columna derecha se ve en assci)
=========================================================
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objdump -s -D main.o
main.o: file format elf32-i386
Contents of section .text:
0000 5589e583 ec0883e4 f0b80000 000029c4 U.............).
0010 c9c3 ..
Contents of section .comment:
_____________________ES lo mismo
/ \
0000 00474343 3a202847 4e552920 332e332e .GCC: (GNU) 3.3. <----version de compilador
0010 35202844 65626961 6e20313a 332e332e 5 (Debian 1:3.3.
0020 352d3133 2900 5-13).
Disassembly of section .text:
00000000 :
0: 55 push %ebp
1: 89 e5 mov %esp,%ebp
3: 83 ec 08 sub $0x8,%esp
6: 83 e4 f0 and $0xfffffff0,%esp
9: b8 00 00 00 00 mov $0x0,%eax
e: 29 c4 sub %eax,%esp
10: c9 leave
11: c3 ret
Disassembly of section .comment:
00000000 <.comment>: //note que aqui la seccion .comment se tomo como codigo pero no lo es
0: 00 47 43 add %al,0x43(%edi) //son los assci de la version de GCC
3: 43 inc %ebx
4: 3a 20 cmp (%eax),%ah
6: 28 47 4e sub %al,0x4e(%edi)
9: 55 push %ebp
a: 29 20 sub %esp,(%eax)
c: 33 2e xor (%esi),%ebp
e: 33 2e xor (%esi),%ebp
10: 35 20 28 44 65 xor $0x65442820,%eax
15: 62 69 61 bound %ebp,0x61(%ecx)
18: 6e outsb %ds:(%esi),(%dx)
19: 20 31 and %dh,(%ecx)
1b: 3a 33 cmp (%ebx),%dh
1d: 2e 33 2e xor %cs:(%esi),%ebp
20: 35 2d 31 33 29 xor $0x2933312d,%eax
...
Objdump es una utileria para tratar con archivos ejecutables disponibles en cualquier distribución de linux, la sintaxis del ensamblador es At&t a diferencia de ndisasm que es sintaxis intel. Sin embargo puede checar que los opcodes son los mismo ya que se trata del mismo archivo.
Para generar un archivo binario "raw" o sin formato el cual contenga únicamente el código ejecutable de TU programa y nada más, lo puedes crear de la siguiente manera:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ gcc main.c
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ld -o main -Ttext 0x0 -e main main.o
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objcopy -R .comment -R .note -S -O binary main main.raw
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm -b 32 main.raw
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 83E4F0 and esp,byte -0x10
00000009 B800000000 mov eax,0x0
0000000E 29C4 sub esp,eax
00000010 C9 leave
00000011 C3 ret
------------------------------------
El contenido en hexadecimal "puro" lo podemos ver mediante
la siguiente orden:
------------------------------------
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.raw | od -x
0000000 8955 83e5 08ec e483 b8f0 0000 0000 c429
0000020 c3c9
0000022
==================================================================
Recordando que el archivo original era:
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main.c -n
1 int main()
2 {
3 }
Entonces, la creacion de una funcion consiste en guardar la base de la pila antigua en la propia pila, despues se utiliza el tope de la pila como la nueva base, esto es lo que hacen las dos primeras instrucciones:
00000000 55 push ebp --el registro ebp apunta a la base de la pila
00000001 89E5 mov ebp,esp --el reg. esp se copia en ebp
--ahora ebp=esp inicializando la nueva pila
Lo anterior es comunmente llamado un "stack-frame" en donde se ve la entrada a una funcion como una entidad independiente capaz de mantener información local mediante su pila, ya que cada base de la pila se mantiene independiente de la anterior.
Recordemos que la pila crece de manera inversa en la arquitectura intel,
entonces para crear una pila más grande debemos "restar" al tope de la pila
para que apunte a direcciones más "bajas", esto significa que si queremos
tener 1 byte más de espacio en la pila debemos restar 1 al registro
Tambien es de notar que los valores de retorno de la función se realizan a traves del registro eax el cual es revisado por el compilador cuando una funcion devuelve un valor. En el anterior ejemplo la definición de la función main era
int main()
{
}
-----------------
GCC introduce por defecto devolver un valor de 0
00000009 B800000000 mov eax,0x0
lo cual significa que main devuelve un entero, entonces
el sistema operativo devolvera un valor entero al programa que
tome lea el valor devuelto por main, el cual podría ser el
propio shell (bash o cualquiera), internamente donde se consultará?,
claro en el registro eax.
Con lo anterior ya sabemos como implementar funciones en un lenguaje de programacion.
Usando el mismo procedimiento podemos ver como GCC implementa variables locales, globales, estructuras, estructuras if, while for, etc. Por el momento podemos ver como genera gcc el código para variables locales dentro de funciones:
Original main2.c
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ cat main2.c
int main()
{
char variable=0xef;
}
====================================================
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ gcc main2.c
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ld -o main2 -Ttext 0x0 -e main main2.o
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ objcopy -R .note -R .comment -S -O binary main2 main2.raw
kb@cobalt:/mnt/hda7/labs/www/escritos/code$ ndisasm -b 32 main2.raw
00000000 55 push ebp
00000001 89E5 mov ebp,esp
00000003 83EC08 sub esp,byte +0x8
00000006 83E4F0 and esp,byte -0x10
00000009 B800000000 mov eax,0x0
0000000E 29C4 sub esp,eax
00000010 C645FFEF mov byte [ebp-0x1],0xef
00000014 0FBE45FF movsx eax,byte [ebp-0x1]
00000018 C9 leave
00000019 C3 ret
Vemos la misma rutina de inicializacion del stack-frame, ademas vemos tambien como el compilador utiliza al registro ebp para referenciar las variables locales:
1)--00000010 C645FFEF mov byte [ebp-0x1],0xef
2)--00000014 0FBE45FF movsx eax,byte [ebp-0x1]
======================
1) char variable=0xef;
2) return variable;
Como vemos la inicializacion "variable=0xef" se hace referenciando a [ebp-0x1], aqui el compilador utilizo un desplazamiento o "resto" 1 a la base de la pila (registro ebp) debido a que la "varianle" esta declarada como "char" que tiene 1 byte de tamaño.
Como habiamos mensionado el mandato "return = varible" hace que el contenido de la variable sea copiado en el registro eax inciso 2)No he mensionado que la salida de la funcion esta a cargo (como ya se habran dado cuenta de la instrucciones:
00000018 C9 leave
00000019 C3 ret
=================
la instruccion leave ( opcode C9) hace un pop del stack-frame
mientras que ret (opcode C3) saca de la pila la dirección de retorno para despues "saltar"(jmp)
a la dirección mensionada.
Con el siguiente programa veremos como las funciones y los operadores como '=' se comunican pasando informacion atraves del registro eax. Veremos tambien como manipular este bloque de informacion de manera tal que simulemos como esta mapeada en el registro mediante una estructura.
cobalt:/mnt/hda7/labs/src/labs1/regs# cat chkreg.c
#include
int main(){
typedef union _reg32 {
unsigned int eax ;
union _reg16 {
unsigned short ax ;
struct _reg8 {
unsigned char al ;
unsigned char ah ;
} reg8;
} reg16 ;
} reg_t ;
reg_t _reg;
_reg.eax = scanreg();
printf("%d bits o %d bytes \n" , sizeof( double )* 8 , sizeof( double) );
printf("------>Contenido de EAX : %x con un tamaño de %d bits | %d bytes \n ", _reg.eax , sizeof(unsigned int)*8 , sizeof(unsigned int) );
printf("------>Contenido de AX : %x con un tamaño de %d bits | %d bytes \n ", _reg.reg16.ax , sizeof(short)*8 , sizeof(short ) );
printf("------>Contenido de Ah : %x con un tamaño de %d bits | %d bytes \n ", _reg.reg16.reg8.ah , sizeof(unsigned char) *8 , sizeof(unsigned char ) );
printf("------>Contenido de Al: %x \n ", _reg.reg16.reg8.al );
return 0 ;
}
cobalt:/mnt/hda7/labs/src/labs1/regs# ./printreg
64 bits o 8 bytes
------>Contenido de EAX : ffefff0 con un tamaño de 32 bits | 4 bytes
------>Contenido de AX : fff0 con un tamaño de 16 bits | 2 bytes
------>Contenido de Ah : ff con un tamaño de 8 bits | 1 bytes
------>Contenido de Al: f0
---------------codigo de scanreg.s
cobalt:/mnt/hda7/labs/src/labs1/regs# cat scanreg.s
global scanreg
section .text
scanreg:
push ebp
mov ebp,esp
mov edx , 0x0ffefff0
mov eax, edx
pop ebp
ret
El codigo anterior muestra mediante la funcion scanreg ( declarada en scanreg.s) guarda el valor 0xffefff0 en el registro eax. Vemos entonces como sacar la informacion que necesitamos sobre los bits que se encuentran en cada uno de los registros dento de eax, confirmando el hecho de que eax es usado por el compilador para devolver los resultados de las funciones en el caso del return.
correo electronico : jorge.garcia.gonzalez@gmail.com