0% encontró este documento útil (0 votos)
356 vistas

Practicas

Este documento describe la herramienta make y cómo se puede usar para organizar proyectos de programación que consisten en múltiples archivos. Explica la estructura típica de directorios de un proyecto y tres casos de uso comunes de make: 1) integración de archivos de texto, 2) integración de archivos de texto y objetos, y 3) integración de archivos de texto y bibliotecas. El objetivo principal de make es automatizar el proceso de compilación para asegurar que solo se recompilen los archivos que necesitan ser actualizados.

Cargado por

muistipunk
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
0% encontró este documento útil (0 votos)
356 vistas

Practicas

Este documento describe la herramienta make y cómo se puede usar para organizar proyectos de programación que consisten en múltiples archivos. Explica la estructura típica de directorios de un proyecto y tres casos de uso comunes de make: 1) integración de archivos de texto, 2) integración de archivos de texto y objetos, y 3) integración de archivos de texto y bibliotecas. El objetivo principal de make es automatizar el proceso de compilación para asegurar que solo se recompilen los archivos que necesitan ser actualizados.

Cargado por

muistipunk
Derechos de autor
© © All Rights Reserved
Nos tomamos en serio los derechos de los contenidos. Si sospechas que se trata de tu contenido, reclámalo aquí.
Formatos disponibles
Descarga como DOCX, PDF, TXT o lee en línea desde Scribd
Está en la página 1/ 132

Sistemas Operativos

Práctica 6: Make

Índice
Objetivos
Herramienta make:
Estructura típica de un proyecto
Organización de proyectos utilizando el make.
Caso 1) Integración de dos archivos tipo texto:
Caso 2) Integración de archivos textos y archivos objeto:
Caso 3) Integración de archivos textos y bibliotecas:
Laboratorio:
Lecturas sugeridas:
Anexo 1 Bibliotecas compartidas
Creación
Compilar programas
Ejecutar programas usando biblitoecas dinámicas
Referencias

Objetivos
Esta práctica se enfoca en la administración de proyectos de programación debido a
que estos sistemas operativos siempre han sido ideales para el desarrollo de
aplicaciones; incluyendo tanto rutinas para servidores como para crear todo un nuevo
servicio o aplicación de usuario desde cero.

Probablemente estás acostumbrado al uso de las herramientas proveídas en


programas como Visual Studio o Netbeans, entre otros, que permiten trabajar con
múltiples archivos fuentes y compilar todos ellos en un único gran proyecto o archivo
ejecutable. Pues bien, estas herramientas con interfaces gráficas heredan sus
funcionalidades de herramientas similares nacidas para la interfaz de línea de
comandos de hace más de una década. MAKE es una de estas herramientas y una
de las más poderosas también.

En esta práctica aprenderás a utilizar dicha herramienta de forma básica para compilar
múltiples rutinas, tal vez desarrolladas por distintos programadores y que dependen
unas de otras; además, aprenderás, tu resultado final podrá incluir librerías estáticas
de rutinas previamente compiladas.

Herramienta make:
Cuando uno está desarrollando una aplicación el objetivo final es crear un solo archivo
ejecutable. Sin embargo, es conveniente dividir el trabajo en pequeños módulos. Estos
módulos finalmente se tendrían que integrar para formar el ejecutable, y pueden ser
archivos en un lenguaje particular (i.e. Lenguaje C), o pueden ser códigos binarios
(e.g. código objeto o bibliotecas de código objeto tales como stdio, que incluye varios
códigos objeto para realizar input/output desde C).En sistemas basados en Unix es
muy común utilizar Make para organizar proyectos compuestos de múltiples módulos.

Make es una herramienta con la que se puede mantener un grupo de códigos


fuentes, códigos objeto y bibliotecas y compilarlos de modo estructurado. Esta
herramienta se encarga de ver qué piezas de software necesitan ser re-compiladas
cuando alguna de ellas cambia y de integrar todos los módulos.

EL comando `make` ejecuta los comandos que se encuentran en el archivo


Makefile en la carpeta donde es invocado.

Empezamos con un ejemplo sencillo de un Makefile, antes de indicar el formato


completo de este archivo:
makefile:
arch.o: arch.c
<tab>cc -c arch.c

Esto nos dice:

Primera línea: “Para lograr el objetivo arch.o (es decir, para poder obtener arch.o), es
necesario que antes exista su versión fuente, arch.c. O sea, arch.o depende de la
existencia previa de arch.c.”

Segunda línea: “Una vez que exista arch.c, se ejecuta el comando cc arch.c (es decir,
se compila arch.c. Eso nos produce arch.o”. Además, el comando nos dice: “si llega a
cambiar arch.c, para obtener arch.o recompile ese arch.c. Si no ha cambiado arch.c,
no es necesario recompilarlo”. Todo eso dicen esas dos líneas.

El formato más completo del Makefile es el siguiente:


objetivo1: dependencias1
<tab>comandos1

objetivo2: dependencias2
<tab>comandos2
(...)

NOTA: TAB es un tabulador, no 5 espacios en blanco. Si se ponen espacios en vez


de TAB, se tendrán problemas de sintaxis.

Los campos denominados “objetivos” son aquellos campos que Makefile puede
ejecutar por separado si se introducen como argumento del comando make. Ejemplo:
“make objetivo1” ejecutará exclusivamente el objetivo “objetivo1”. En el ejemplo
anterior, podríamos entonces decir “make arch.o”. Ahora, las dependencias son otros
archivos u objetivos que deben ya existir sin cambio o que se deben de ejecutar antes
de poder realizar el objetivo en cuestión. Los comandos son por lo regular comandos
para compilar uno o más códigos fuentes, incorporar códigos objetos, o crear o
incorporar bibliotecas dinámicas o estáticas entre otros.

Si en el ejemplo después modificamos arch.c y simplemente ejecutamos el comando


"make arch.o", este comando make se da cuenta de que arch.c fué modificado, y
recompila. Esto es debido a que el comando make es capaz de detectar si un objetivo
ya fue cumplido o si sus dependencias fueron modificadas y necesita re-evaluarse
y de forma interna lleva un registro de estos cambios. De tal forma que si se tiene un
proyecto grande cuya compilación completa tardaría mucho tiempo, “Make” tratará de
ahorrar recursos al no re-evaluar objetivos ya cumplidos (Si sus dependencias no han
sufrido cambios).

Existe un objetivo en particular denominado “all” y cuando el comando make se invoca


sin argumentos asume que debe ejecutar dicho objetivo. Si “all” no existe (no lo
definió el usuario) entonces evalúa el primer objetivo definido en el archivo Makefile.
Recuerda que en el momento en que los objetivos son evaluados, se procede a
verificar sus dependencias; estas pueden ser nombres de archivos u otros objetivos.
Nuevamente regresando al ejemplo, podríamos simplemente haber dicho “make” en
vez de “make arch.o” pues solo teníamos ese objetivo.

El archivo Makefile se utiliza para estructurar las dependencias entre módulos de tal
modo que sólo se vuelvan a compilar aquellos módulos que hayan sido modificados.
Podemos ver el comando como una especie de “shell scripting” enfocado
totalmente a compilación e integración de proyectos (en este laboratorio
utilizaremos solamente C/C++ pero Makefiles también puede funcionar para otros
lenguajes como Java)..

Curiosidad: Makefile es ampliamente utilizado; de hecho si has


tenido oportunidad de bajar a Linux el código fuente de alguna
aplicación para instalarla forma manual, entonces uno de los pasos
que seguramente debiste seguir habrá sido el uso de make,
seguramente con “make [all], o make install” que se encargan de
compilar todo los archivos fuentes y las modificaciones requeridas de
los registros de tu sistema operativo.

Estructura típica de un proyecto


Durante esta práctica crearemos diferentes proyectos los cuales tendrán una
estructura de directorios similar a la siguiente:
Proyecto
| - bin
| - doc
| - objetos
| - sources
| - miIncludes

El objetivo de utilizar esta estructura es acostumbrarte a una separación de piezas


de los proyectos que estés generando. Cada carpeta tiene una utilidad especial (y lo
puedes comprobar en cualquier proyecto que instales de forma manual) y es la
siguiente:
 bin - Todos los archivos ejecutables (binarios) que tenga el proyecto son depositados
en este folder.
 doc - Toda la documentación que se libere del proyecto suele ser depositada en este
directorio.
 sources - El código fuente del proyecto (sea un archivo, 10, 100, 500, etc) iría en
este lugar.
 objetos - Se utiliza como un directorio temporal donde se irán guardando los códigos
objetos que se vayan creando como pasos previos a la creación del/los código(s)
ejecutable(s) final(es).
 miIncludes - Este es un nombre arbitrario, pero se intenta separar los códigos fuentes
del proyecto de aquellos códigos conseguidos de terceros (sea en código fuente,
objeto y librerías, dll, etc.), y que son incluídos también como parte del proyecto.
Generalmente en la carpeta raíz del proyecto existe un Makefile que estará
preparado para ejecutar todo lo necesario (incluso otros makefiles). Para este
laboratorio omitiremos ésto y se crearán makefiles directamente en los directorios
Sources y misIncludes según sea necesario.

Organización de proyectos utilizando el make.


La unión de los módulos para integrar un archivo ejecutable depende del tipo de
archivo. Esta parte puede ser lo que más te confunda si no estás bien entrenado en
las diferentes fases de compilación de un archivo fuente a un proyecto ejecutable; si
tienes dudas pregunta, o lee la práctica relacionada con el complemento de C.

Como quiera, estos módulos pueden ser otros archivos de código fuente, código
binario ( archivo objeto o biblioteca) y se pueden apreciar como los siguientes:

a) Si el módulo que se desea integrar es un archivo tipo texto, entonces se


utiliza un comando del preprocesador para que realice la integración de los
módulos antes de compilar. Ejemplo:
#include <stdio.h>
#include “biblioteca_no_estandar.h”

b) Si el módulo que se desea integrar es un archivo objeto, entonces la


integración de los módulos se realiza en la última etapa de la compilación.
Ejemplo:
gcc -o ejemplo modulo1.o modulo2.o modulo3.o

indica ligar en un solo módulo objeto “ejemplo” otros tres


objetos.

c) Si el módulo que se desea integrar es una biblioteca, entonces la


integración de la biblioteca con los otros módulos también se realiza en la
etapa final de la compilación, utilizando la opción -l<nombre de biblioteca>.
Ejemplo:
gcc -o coseno -lm coseno.c
indica integrar la biblioteca “coseno.c” que parece
incluír un solo módulo; en general, las bibliotecas
incluyen varios módulos.
Cada uno de los casos se verán a continuación. Respecto a usarlos como prueba,
puede presentar problemas el copiado de las líneas, así que si prefieres puedes
descargar un archivo .tar que posee una copia sin errores en esta liga: Archivos de los
3 casos.

.o ?

Posiblemente como alumno esté acostumbrado al código C/C++, pero por el objetivo
de las clases donde aprendió a programar su código basaba de estar en Texto a ser
código ejecutable. De esa etapa a la ultima existen varias sub-etapas que se utilizan y
están identificadas en el documento “Complemento: Programación C”
En general, las fases por las que pasa todo código en C hacia el archivo ejecutable
son:

1. Pre-procesamiento - Solo aparece lenguaje C, un único archivo


2. Pre-compilador - Todo el lenguaje de alto nivel es traducido a lenguaje
mnenomico de bajo nivel (Aún es texto).
3. Ensamblador - El lenguaje maquinal se convierte en código objeto
(Propiamente un archivo que tiene instrucciones maquinales usualmente con la
extensión .o).
4. Ligado - Se crea un único archivo ejecutable final.

5. Una programación avanzada, por ejemplo para el trabajo de un S.O. permite


interrumpir el proceso de compilación de C en las primeras dos fases para
modificarlo. La tercera fase es de mucha utilidad pues permite el compartir código
objeto sin necesidad del código fuente. Pueden ver un ejemplo de estas fases en esta
liga.

Caso 1) Integracion de dos archivos tipo texto (Método


erróneo):
Para este caso considera la siguiente organización de directorios:

miProj
| - bin
| - doc
| - sources
| | - proj.c
| |- makefile
|- miIncludes
|- aritReal.h

En el directorio bin quedará el programa integrado (códigos ejecutables o binarios).

En el directorio sources se encuentrán el archivo fuente principal (proj.c) y un archivo


Makefile para los archivos fuente.

El directorio miIncludes contiene todas los archivos locales al proyecto, que se


pueden incluir al programa principal utilizando la directiva #include de lenguaje C.

A continuación se muestra el código fuente de los archivos: aritReal.h y proj.c

/* archivo 'aritReal.h' es almacenado en el directorio 'miIncludes' */

/* función que suma dos números. */

float suma(float a, float b){


return(a+b);
}

/* función que resta 'b' a 'a' */

float resta(float a, float b) {


return(a-b);
}

/* función que multiplica dos números */


float multiplica(float a, float b){
return(a*b);
}

/*función que divide 'a' por 'b'*/

float divide(float a, float b) {


return(a/b);
}

Ahora, se muestra el archivo proj.c que invoca al archivo anterior:


/* archivo 'proj.c' almacenado en el directorio sources. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../miIncludes/aritReal.h" /* include definido por el usuario. */

int main(int argc, char **argv)


{
float a,b,c,d;

a= suma(3.5,3.4);
b= resta(4.0,2.0);
c= multiplica(3.0,5.0);
d= divide(7.0,3.0);

fprintf(stdout," a= %f\n b= %f \n c= %f \n d= %f\n", a, b, c, d);


}

Es muy común que el archivo Makefile se utilice para ver dependencias entre archivos
y así solo se compilen aquellas partes del código que fueron afectadas. En este
ejemplo, el archivo Makefile(1) se muestra a continuación:
CC= gcc #/*string que se substituye mas adelante dondequiera que aparezca*/
ARGS=-O #/*otro string. Indica: "optimiza el código" */

proj: proj.o ../miIncludes/aritReal.h


<tab>$(CC) -o ../bin/proj $(ARGS) proj.o #borrar <tab> y oprimir la tecla Tab

proj.o: proj.c
<tab>$(CC) $(ARGS) -c proj.c #borrar <tab> y oprimir la tecla Tab

clean:
<tab>rm *.o #borrar <tab> y oprimir la tecla Tab

Recordatorio: No olvidar el espacio TAB al inicio de las líneas siguientes a las


etiquetas

En español, este archivo Makefile nos dice que: “para obtener proj (primer
objetivo), es necesario que existan proj.o y aritReal.h. Una vez que estos dos
existen, se obtiene proj utilizando el compilador gcc, produciéndose como
objeto proj (-o ..bin/proj) a partir de proj.o. Ahora bien, para que pueda existir proj.o
(segundo objetivo), se requiere que exista proj.c; una vez que existe proj.c,
simplemente se compila. Si cambiara proj.c, al ejecutar el comando "make” se
recompilaría. Si cambiara arithReal.h, pues también se recompilaría proj.c
automáticamente, ya que el primer objetivo nos indica que arithReal.h es requisito para
poder obtener proj y que éste depende de proj.o y que este último depende de proj.c
que ya cambió pues incluye a arithReal.h”. . . Bueno no se si lo prefieres así en
español o mejor de manera más formal:

 CC y ARGS son constantes textuales que se sustituirán por sus valores donde
quiera que aparezcan en el cuerpo del texto
 proj es un objetivo que tiene dependencia del código objeto, proj.o, y del
archivo ../miIncludes/aritReal.h. Es decir, si se ejecuta desde la línea de
comandos "make proj" y se ha modificado alguno de estos dos archivos,
entonces se ejecuta el comando que sigue, el cual obtiene el objetivo proj.
 El compilador nos dice que el código ejecutable se almacenará en el
directorio ../bin/.
 Nos dice que el archivo objeto proj.o tiene dependencia del código fuente
proj.c. Es decir, que si se modifica el código fuente se obtendrá el código objeto
de este fuente.

 Además, nos dice que el objetivo clean no tiene dependencias y por lo tanto,
cada vez que ejecutemos esta opción ("make clean")se realizará el siguiente
comando rm *.o, que borrará del directorio todos los archivos objeto

Ahora, para obtener el código del proyecto se utiliza el comando make , como sigue:
user@localhost> cd miProj/sources
user@localhost> make
cc -O -c proj.c
cc -o ../bin/proj proj.o
user@localhost>

después de ejecutar el comando make exitosamente se puede observar que se creó el


archivo proj en el directorio ../bin, y el programa objeto proj.o esto es:
miProj
| - bin
| |- proj
| - doc
| -sources
| | - proj.c
| | - proj.o
| |- Makefile
| -miIncludes
|funciones

El archivo objeto se puede borrar como sigue:


user@localhost> cd miProj/sources
user@localhost> make clean

para ejecutar proj:


user@localhost> ../bin/proj
a= 6.900000
b= 2.000000
c= 15.000000
d= 2.333333
user@localhost>

Como el comando make tiene las dependencias entre archivos, entonces si


intentamos volver a hacer el make, no ejecutará ninguna compilación, ya que no
hemos modificado ni el archivo aritReal, ni el archivo proj.c.
ADVERTENCIA: Por propósitos didácticos, hemos usado un archivo denominado ArithReal.h
que incluye el código fuente de varias funciones -suma, resta etc. Usualmente sin embargo,
estos archivos “.h” sólo incluyen la primera línea de estas funciones, y el código de éstas
proviene de otro lado. Los casos 2 y 3 ya se manejan de esta forma .

En resumen, este caso compila directamente un archivo fuente el cual utiliza


(de forma no usual) una librería que contiene código fuente a compilar junto
con el archivo que lo incluye.

Caso 2) Integración de archivos textos y archivos objeto:


Este caso ya maneja un modo adecuado y correcto de utilizar las librerías, la
función es similar pero existen más códigos fuentes que en el caso anterior y el
make primero crea los códigos objetos y después los compila en un único archivo
ejecutable.

Ejemplo:
Se utilizará el directorio objetos para almacenar el archivo fuente y el objeto de la
biblioteca, y el directorio sources para almacenar el fuente y objeto del programa
proj.c. Entonces, la organización de los directorios del proyecto es como sigue:
miProj
| - bin
| - doc
| - objetos
| | - aritReal.c
| |- Makefile
| - sources
| | - proj.c
| |- Makefile
| - miIncludes
| | - aritReal.h

El archivo Makefile(2) en el directorio objetos contiene inicialmente el código fuente


del archivo aritReal.c

El archivo Makefile(2) se muestra a continuación:


CC=gcc
aritReal.o: aritReal.c
<tab>$(CC) -c aritReal.c

clean:
<tab>rm *.o

Nota: En el documento nos referimos a este archivo como Makefile(2) para


diferenciarlo de los demás casos, pero en la carpeta debe llamarse
“Makefile”.

para obtener el código objeto, se hace lo siguiente:


user@localhost> make aritReal.o
cc -c aritReal.c

Después de ejecutar el comando make, podemos ver que en el directorio se encuentra


el código ejecutable del archivo aritReal.c, esto es:
user@localhost> cd miProj/objetos
user@localhost>ls
aritReal.c aritReal.o Makefile

o en forma de árbol:
miProj
| - bin
| - doc
| - objetos
| | - aritReal.c
| | - aritReal.o
| |- Makefile
| - sources
| | - proj.c
| |- Makefile

Ahora que tenemos el código objeto, nos vamos al directorio miProj/sources. En este
directorio se encuentran el código fuente proj.c y el archivo Makefile de nuestro
proyecto.

A continuación se muestra el contenido del archivo Makefile(3):


CC=gcc
ARGS=-O
proj:proj.o ../objetos/aritReal.o
<tab>$(CC) -o ../bin/proj proj.o ../objetos/aritReal.o

proj.o: proj.c
<tab>$(CC) $(ARGS) -c proj.c

clean:
<tab> rm *.o

 Las dependencias de proj son sobre los código


objeto proj.o y ../objetos/aritReal.o.
 Al variar alguno de estos archivos se compila el archivo proj.c y se liga con los
códigos objeto proj.o aritReal.o.

La ejecución del comando make sería como se muestra a continuación:

user@localhost> make proj


cc -O -c proj.c
cc -o ../bin/proj proj.o ../objetos/aritReal.o
user@localhost>

A continuación se muestra el archivo fuente proj.c


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "...objetos/aritReal.h"

int main(int argc, char **argv)


{
float a,b,c,d;

a= suma(3.5,3.4);
b= resta(4.0,2.0);
c= multiplica(3.0,5.0);
d= divide(7.0,3.0);
fprintf(stdout,"a= %f\n b= %f \n c= %f \n d= %f\n", a, b, c, d);
}

Aunque el encabezado aritReal.h está presente al igual que en el caso anterior, ahora
solo posee los encabezados de las funciones como se muestra a continuación
/* archivo 'aritReal.h' es almacenado en el directorio 'miIncludes' */

/* función que suma dos números. */


float suma(float a, float b);

/* función que resta 'b' a 'a' */


float resta(float a, float b);

/* función que multiplica dos números */


float multiplica(float a, float b);

/*función que divide 'a' por 'b'*/


float divide(float a, float b);

Aunque en el algunos casos se puede omitir hacer referencia a la librería se


recomienda no hacerlo ya que pueden haber errores para el manejo de las funciones,
lo cual se puede reflejar en valores inválidos como se muestra en la siguiente captura
errónea:

user@localhost> proj
a= 1074528256.000000
b= 1074790400.000000
c= 1074266112.000000
d= 1075576832.000000

Posiblemente se esté preguntado ¿por qué utilizamos 2 archivos “Makefile”?


Técnicamente no hay necesidad de hacerlo; un único archivo nos permitirá primero
cumplir los objetivos de crear los códigos objetos de la librería y después ligarlos al
código objeto de nuestro código principal. La razón detrás de esta decisión es que
usualmente cuando nos dan librerías, pueden no llegar a darnos los códigos fuentes
sino solamente los códigos objeto; esto es muy común ya que la librería pudo haber
sido desarrollada por otra empresa que desea es proteger el código fuente que ha
desarrollado con sus propios recursos. Por eso hemos separado en dos partes el
proceso de compilación: una parte que representa nuestro propio proyecto y en otra
parte que representa la compilación de la empresa que nos proporciona su código.

En resumen, en este escenario generamos nuestro código objeto de nuestro proyecto


y este se le añade el código objeto de la librería que en este caso es generado con
otro archivo Makefile independiente.
Caso 3) Integración de archivos texto y bibliotecas:
Este es un caso común y en otros lenguajes es como si consiguieran el archivo .dll o
los .class listos para integrarse al código de su proyecto. En este caso muy en
particular utilizaremos una biblioteca (archivo binario) generada de forma estática y
nuestro código fuente.
Para este tercer caso, se maneja una biblioteca la cual puede estar constituida de la
combinación de todos los códigos objetos de una compañía particular en un único
archivo; dicho archivo es la biblioteca que nos proporciona esa compañía. El detalle
viene en que los S.O. modernos se manejan dos tipos de librerías: dinámicas
(usualmente compartidas ) y estáticas. Las primeras tienen como objetivo ahorrar
espacio de memoria en aquellas bibliotecas que son utilizadas por más de un
programa; por lo general la biblioteca se anexa al programa en el momento de la
ejecución del mismo.

En el caso de las librerías estáticas, las librerías son anexadas al programa que las
utiliza al final del proceso de compilación. Esta fase final se denomina “ligado” o
“linking” en inglés. En este escenario, más de un programa puede utilizar la misma
librería pero estarán duplicadas en cada programa.

Para objetivos de esta práctica se manejarán exclusivamente librerías estáticas. Para


mayor información de cómo crear una biblioteca dinámica ver el Anexo1 al final del
documento.

Ejemplo:

Crear un biblioteca que contenga el código almacenado en el archivo aritReal.o.


La organización del proyecto es similar a la del ejemplo anterior:

miProj
| - bin
| - doc
| - biblioteca
| | - aritReal.c
| |- Makefile
| - sources
| | - proj.c
| |- Makefile
| - miIncludes
| | - aritReal.h

El archivo Makefile(4) del directorio miProj/biblioteca contiene la siguiente información:


modules= aritReal.o
all:libreria

aritReal.o: *.c
<tab>gcc -c -Wall *.c

libreria: $(modules)
<tab>ar -cvq libMia.a *.o
clean:
<tab>rm *.o
<tab>rm libMia*

Observe que en esta ocasión se están utilizando dos programas distintos: gcc y ar; el
primero es el compilador mientras el segundo se utiliza para crear una biblioteca que
contenga el resultado de las compilaciones. Se ha añadido el objetivo "all" para
garantizar que el uso del comando make genere los códigos objetos y la creación de
la librería (en realidad, inicia la creación de librería pero para lograrlo debe cumplir
primero con el objetivo aritReal.o).

 La opción “-Wall” dentro del objetivo "aritReal.o" indica al compilador que se


compilen todos los codigos fuente obteniéndose los códigos objeto
correspondientes. En este caso hay un solo código fuente a compilarse pero en
el caso general podría haber varios.
 El objetivo librería se encarga de colocar todos los códigos objeto dentro de la
que será la librería estática libMia.a

Para crear la biblioteca libMia.a:

user@localhost> ls
aritReal.c Makefile
user@localhost> make
gcc -c -Wall *.c
ar -cvq libMia.a *.o
user@localhost> ls

aritReal.c aritReal.o libMia.a Makefile

user@localhost>

Ahora, que ya se creó la biblioteca nos cambiamos al directorio miProj/sources. El


archivo proj.c está desplegado a continuación:
/* proj.c */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include "../miIncludes/aritReal.h"

int main()
{
float a,b,c,d;

a= suma(3.5,3.4);
b= resta(4.0,2.0);
c= multiplica(3.0,5.0);
d= divide(7.0,3.0);
fprintf(stdout,"a= %f\n b= %f \n c= %f\n d=%f\n", a, b, c, d);
}

El archivo Makefile(5) se muestra a continuación:


DIR=../biblioteca
CC=gcc

proj: proj.o
$(CC) -o ../bin/proj proj.o -L$(DIR) -lMia

proj.o: proj.c

clean:
rm *.o

 La opción -lMia indica que se va a utilizar una biblioteca extra diferente a la


biblioteca estándar y lleva el nombre libMia.a
 La opción -L sirve para indicar un directorio donde puede encontrar la
biblioteca.
 El objetivo proj.o solo tiene como dependencia proj.c. Como puede observar se
genera el código objeto sin necesidad de una linea de gcc. Esto es automático
cuando el objetivo es un objeto y la dependencia un solo fuente del mismo
nombre que el objetivo.

Para compilar el programa se hace lo siguiente:


user@localhost> cd miProj/sources
user@localhost> make

gcc -c -o proj.o proj.c


gcc -o ../bin/proj proj.o -L../objetos -lMia
user@localhost>

El archivo cabecera aritReal.h deja de ser una opción y se muestra a continuación:


/* archivo 'aritReal.h' es almacenado en
el directorio 'miIncludes' */

/** funcion que suma 'a' y 'b' */


float suma(float a, float b);

/** funcion que le resta 'b' a 'a' */


float resta(float a, float b);

/**función que multiplica 'a' y 'b' */


float multiplica(float a, float b);

/** función que divide 'a' por 'b' */


float divide(float a, float b);

La organización de proyecto se vería así:


miProj
| - bin
| - doc
| - objetos
| | - aritReal.c
| | - aritReal.o
| | - libMia.a
| |- Makefile
| - sources
| | - proj.c
| |- Makefile
| -miIncludes
|- aritReal.h

A continuación se muestra la codificación del programa principal, proj.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#include "../miIncludes/aritReal.h"

int main()
{
float a,b,c,d;

a= suma(3.5,3.4);
b= resta(4.0,2.0);
c= multiplica(3.0,5.0);
d= divide(7.0,3.0);

fprintf(stdout,"a= %f\n b= %f \n c= %f \n d=%f %f\n", a, b, c, d);


}

En resumen este caso es similar al caso 2, se compila el código fuente de nuestro


proyecto y a este se le vincula con la librería previamente creada mediante otro
makefile (por la forma en que representamos de donde salió la librería). En este
escenario el encabezado (aritReal.h) es obligatorio.

= = = = Laboratorio = = = =
 Actividades a realizar en esta práctica se encuentran descritas en este
documento, sus respuestas deben registrarse en :

o Material de Apoyo: Lab-06: Make

 Lee el manual de make


 Desarrolla el programa numerosUno.c que calcule el cuadrado, el cubo y la
cuarta potencia del número 2. Los valores se calcularán usando las funciones
especificadas en la Tabla 1:

Nombre de la Nombre del ¿Qué hace?


función archivo que
lo contiene
cuadrado cuadrado.c Función que recibe como entrada un número y
regresa su cuadrado

cubo cubo.c Función que recibe como entrada un número y


regresa su cubo

cuarta cuarta.c Función que recibe como entrada un número y


regresa el número elevado a la cuarta.

Tabla 1. Funciones matemáticas.

Ejercicio 1
Los resultados se deberán imprimir en pantalla y guardar en el archivo salidaUno.txt. Una
vez que tengas el los archivos de código: numerosUno.c, cuadrado.c, cubo.c, cuarta.c (nota
que son cuatro archivos separados, no uno solo con todas estas funciones), crea un archivo
único Makefile que te permita compilar todos los archivos.

Primero define tres reglas que obtengan el código objeto de los archivos cuadrado.c, cubo.c y
cuarta.c; y luego define otra regla para crear el programa numerosUno a partir de los archivos
numerosUno.c, cuadrado.o, cubo.o y cuarta.o. También define una regla clean para borrar los
archivos objeto después de la compilación.

Ejercicio 2
Desarrolla la biblioteca potencias, que contiene las funciones especificadas en la Tabla 1 (un
único archivo). A partir de el programa anterior, desarrolla el programa numerosDos.c, que
realiza las mismas operaciones aritméticas, pero que utiliza la biblioteca potencias en lugar de
los archivo objeto como fuente de código para las funciones aritméticas.

Lecturas sugeridas:
La siguiente liga posee ejemplos de Make: http://mrbook.org/tutorials/make/

Anexo 1 - Bibliotecas compartidas


Creación
Para crear una biblioteca dinámica se necesita tener disponibles los archivos objetos
generados con las banderas -fPIC o fpic la cual habilita el posicionamiento del código
independiente. La bandera -Wl sirve para mandarle opciones al programa encargado de hacer
las ligas (linker) y no debe tener espacios sin escapar. Para generar la biblioteca se usa el
siguiente comando.

gcc -shared -Wl,-soname,your_soname \ -o library_name file_list library_list


donde el "your_soname" es el nombre de la biblioteca compartida que comienza con "lib", el
nombre de la biblioteca y seguida por la extensión ".so"

Compilar programas
Para compilar programas que hagan uso de una biblioteca compartida es necesario utilizar la
bandera -L para especificar el lugar donde se encuentra dicha biblioteca.

gcc -L /path/a/bilbioteca/ -libBiblioteca código.c

Ejecutar programas usando bibliotecas dinámicas


Si no se instaló la biblioteca en un lugar estándar se puede utilizar la variable de entorno
LD_LIBRARY_PATH para especificar dónde se puede encontrar la biblioteca que se necesita
para ejecutar el programa.

LD_LIBRARY_PATH=/path/a/mi/biblioteca/;$LD_LIBRARY_PATH mi_programa

Referencias

http://www.dwheeler.com/program-library/Program-Library-HOWTO/x36.html

Laboratorio de
Sistemas Operativos

Práctica 7: Estrategias de I/O

Objetivo
En esta práctica nos enfocaremos en los elementos básicos para el desarrollo de
aplicaciones para ambiente GNU/Linux utilizando el lenguaje nativo del mismo: C.
Nos enfocaremos principalmente en diferenciar el uso de código de usuario y código
de sistema.

Índice
Desarrollo de programas usando Programación de sistemas
---- Algunas llamadas a sistema---
Lectura mediante read( )
Escribiendo con write( )
Cerrar archivos
--- Un ejemplo de llamadas al sistema ---
File I/O: writeback , Synchronized I/O
Synchronized I/O
Lecturas predictivas
I/O usando buffers (lenguaje C estándar)
Apuntador a archivo
Leyendo Streams
Escribiendo a un stream
La biblioteca estándar de C implementa varias funciones para escribir en un
stream abierto.
Ejemplo de un programa que usa I/O en buffers.
Mapeo a Memoria (Memory map)
Referencias
Fuentes adicionales

Desarrollo de programas usando Programación de


sistemas
Programación a nivel-Sistemas Linux (Linux System-Level Programming or Linux
System Programming) es el arte de escribir software del Sistema.

 Software del sistema: es aquel que está en contacto directo con el manejo del
kernel y las bibliotecas del sistema. El desarrollo de software de sistema exige
que el usuario conozca sobre el hardware del sistema en el que va a trabajar el
software.

Ejemplos de software de sistema incluyen:


 El sistema operativo
 Los interpretadores de comandos
 Los sistemas de interface gráfica
 Los compiladores, ensambladores, controladores de versión, los
depuradores, etc.

 Programación de Aplicaciones (Application Programming) : es el arte de


escribir software de Aplicaciones. Un software de aplicación es un programa
auto-contenido que realiza una función específica directamente con el usuario.
Ejemplos incluyen:
 Hojas de cálculo (i.e. Excel)
 Software para procesamiento de texto (i.e. Office)
 Software para Presentación (i.e. PowerPoint)
 Aplicaciones de red como correo electrónico, telnet y WWW.

Desde el punto de vista del programador las diferencias entre “Programación de


Sistemas” y “Programación de Aplicaciones” es que la “Programación de Sistemas”
requiere conocimiento sobre el Hardware y el Sistemas Operativo en el que se
desarrollará el programa. Otra diferencia son las bibliotecas a utilizar. Por otro lado,
se deben de emplear las mismas buenas prácticas de programación.

La mayoría del código para Linux y Unix se escribe “a nivel sistema”. Ya que las
llamadas al sistema son dependientes del hardware, mucho de este código se
escribe en lenguaje C.

La programación a nivel sistema se hace a través de llamadas al sistema (system


calls or supervisor calls).

Las llamadas al sistema son funciones que se invocan desde nivel usuario hacia el
kernel para requerir un servicio o recurso del sistema operativo.

Ejemplos de llamadas al sistema son:


 Sistema de Archivo: open(), read(), write(), close(), etc.
 Procesos; fork(), getpid(), set_thread_area(), etc.
 Red: socket(), bind(), etc.

Cada Sistema Operativo define diversas llamadas al sistema. Por ejemplo en el caso
de sistemas operativos Windows, se definen miles de llamadas al sistema. En el caso
de Linux el número de llamadas al sistema son aproximadamente unas 300.

NOTA: La última afirmación corresponde a la era de la familia Windows 9x. Actualmente, se


puede ver una lista recopilada por metasploit de llamadas de sistemas de windows en esta liga.

---- Algunas llamadas a sistema---


La documentación completa de estas llamadas a está en los manuales del
programador (“man pages”). En ubuntu estas páginas no pueden no haber sido
instaladas automáticamente, se deben instalar los paquetes man-db y manpages-dev

 Abrir archivos:
Antes que los datos de un archivo se puedan leer, éste debe de ser abierto utilizando
las llamadas a sistema open() o create().

int open (const char *name, int flags);


int open (const char *name, int flags, mode_t mode);

 Si se puede abrir exitosamente, regresa un descriptor de archivo.


 Comienza en la posición 0 del archivo, y
 El archivo se abre de acuerdo al contenido del parámetro “flags”.

La variable “flags” puede contener diferentes valores dependiendo de la operación


que deseamos realizar sobre el archivo: O_RDONLY, O_WRONLY, or O_RDWR.

La variable modo puede tomar los siguientes valores: O_APPEND, O_CREAT,


O_EXCL ú O_TRUNC. La información detallada se encuentra en man open .

La definición de estas funciones, así como las constantes utilizadas en los “flags”, se
pueden encontrar en los siguientes encabezados.
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>

Lectura mediante read( )

El mecanismo más básico y comúnmente usado para leer, es la llamada a sistema


read(), se define:
#include <unistd.h>
ssize_t read (int fd, void *buf, size_t len);

Cada llamada lee hasta una cantidad de len bytes a partir de la posición actual
referenciada por el descriptor de archivo fd, y los guarda en buf. Regresa el número de
bytes escritos en buf, o -1 si ocurre un error.

Escribiendo con write( )

La llamada a sistema más básica y común es write(). Se define:


#include <unistd.h>
ssize_t write (int fd, const void *buf, size_t count);
La llamada a write() escribe una cantidad de count bytes a partir de la posición actual
referenciada por fd. Regresa el número de bytes escritos, o -1 en caso de error.

Cerrar archivos

Cuando un programa ha terminado de trabajar con un file descriptor, debe desasociar


el file descriptor del archivo mediante la llamada a sistema close():

#include <unistd.h>
int close (int fd);

Regresa un 0 en caso de éxito, o un -1 en caso de error.

--- Un ejemplo de llamadas al sistema ---


Este ejemplo es una codificación simple del comando cp:

/* PROGRAMA: copiar.c
FORMA DE USO:
./copiar origen destino
VALOR DE RETORNO:
0: si se ejecuta satisfactoriamente.
-1: si se da alguna condicion de error
*/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h> /* open() */
#include <sys/stat.h> /* open() */
#include <fcntl.h> /* open() */
#include <unistd.h> /* read() y write() */

char buffer[BUFSIZ]; /*Buffer para manipular datos. */

int main(int argc, char *argv[])


{
int fd_origen;
int fd_destino;
int nbytes;

/*Analisis de los argumentos de la linea de comandos*/


if (argc != 3) {
fprintf(stderr,"Forma de uso: %s origen destino\n", argv[0]);
exit(-1);
}

/*Apertura del archivo en modo solo lectura (O_RDONLY). */


if ((fd_origen=open(argv[1],O_RDONLY))== -1) {
perror(argv[1]);
exit(-1);
}

/* Apertura o creacion de archivos en modo solo escritura. */


/* Abrir en modo solo Lectura (O_WRONLY). */
/* Si el archivo existe entonces truncar a cero bytes (O_TRUNC).*/
/* El archivo es creado en caso de que no exista (O_CREAT). */
/* El modo que se crea es 0666. */
if ((fd_destino=open(argv[2],O_WRONLY|O_TRUNC|O_CREAT,0666))== -1) {
perror(argv[2]);
exit(-1);
}

/* copiamos el archivo origen en el archivo destino. */


while ((nbytes=read(fd_origen, buffer, sizeof buffer))> 0)
write(fd_destino, buffer, nbytes);

close(fd_origen);
close(fd_destino);
}

A la hora de ejecutar, observe que cuando no hay error, el programa envía al sistema
operativo el valor 0, y si hay error el programa envía al sistema operativo el valor de -1.
El sistema operativo toma el valor y lo almacena en la variable $?. Entonces, el SO
puede utilizar esta información en un programa shell para tomar decisiones. A
continuación se muestra el ejemplo de un caso con error:

user@localhost ~$ ./copia copiar.c copiar2.c


user@localhost ~$ echo $?
0
user@localhost ~$ ./copia copiar3.c copiar2.c
copiar3.c: No such file or directory
user@localhost ~$ echo $?
255

File I/O: writeback , Synchronized I/O


Comportamiento del write (writeback)

La disparidad en el rendimiento entre procesadores y discos duros haría que para


efectos de rendimiento se retrase la escritura. Cuando una aplicación en el espacio del
usuario realiza una llamada al sistema write(), el kernel de Linux realiza un par de
revisiones, y entonces simplemente copia los datos en un buffer interno al sistema
operativo. Después, en segundo plano, el kernel toma todos los buffers “sucios” de
todos los usuarios (es decir, que no han sido aún escritos a disco), los ordena
óptimamente (de tal forma que la cabeza escritora del disco duro minimice su
trayectoria), y los escribe en el disco. Este proceso se conoce como writeback. Esto
permite al kernel aplazar las escrituras a periodos más desocupados y procesar en
lote muchas escrituras juntas.

Este comportamiento realmente mejora el rendimiento, de la misma forma en que un


read puede leer de la memoria caché sin tener que ir al disco. Las peticiones de
read y write se intercalan según lo previsto, y los resultados son los esperados – eso
sí el sistema no tiene una falla antes de que los datos lleguen al disco...

 Aún cuando una aplicación pueda creer que se realizó la escritura con éxito, en
caso de falla los datos podrían no haber llegado al disco.

 Otro problema con las escrituras retrasadas es la incapacidad de hacer cumplir


el orden de escritura. El kernel podrá re-ordenar las solicitudes de escritura
como lo considere oportuno. Normalmente esto es un problema solo si el
sistema tiene una falla, ya que eventualmente todos los buffers son escritos.

 Un problema final con las escrituras retrasadas tiene que ver con el reporte de
ciertos errores de I/O. Cualquier error de I/O que ocurra durante la escritura a
disco —por ejemplo, una falla física de disco— no puede ser reportada al
proceso que emitió la petición de escritura.

El kernel trata de minimizar los riesgos de estas escrituras diferidas. Para asegurar
que los datos son escritos oportunamente, el kernel establece una edad máxima del
buffer, y escribe a disco todos los buffers “sucios” antes de que maduren más allá del
tiempo dado. Los usuarios pueden configurar este valor en el archivo
/proc/sys/vm/dirty_expire_centisecs. El valor está especificado en centésimas de
segundo.

Synchronized I/O
Aunque la sincronización de I/O es un tema importante, los problemas asociados con
las escrituras retrasadas no deben ser temidos. Las escrituras mediante buffers
proporcionan una mejora enorme en el rendimiento, y consecuentemente, cualquier
sistema operativo que merezca la etiqueta de “moderno”, implementa escrituras
diferidas mediante buffers. Sin embargo, hay ocasiones en las que las aplicaciones
desean controlar de manera precisa el momento en que los datos llegan al disco. Para
esos usos, el kernel de Linux proporciona un puñado de opciones que permiten
negociar el rendimiento por medio de las operaciones sincronizadas.

fsync() asegura que todos los datos sucios asociados con el archivo mapeado por el
file descriptor fd sean escritos al disco en el momento en que se ejecuta esa llamada.
Menos óptimo, pero de más amplio alcance, la llamada a sistema sync() se
proporciona para sincronizar todos los buffers al disco.

Otra posibilidad es asignar la bandera O_SYNC en la llamada a sistema open(),


indicando que todas las operaciones I/O deben ser sincronizadas. Por ejemplo, en el
programa copiar.c, cuando el archivo de escritura es abierto:

if ((fd_destino=open(argv[2],O_WRONLY|O_TRUNC|O_CREAT,0666))== -1)
podríamos incluir la bandera O_SYNC de la siguiente forma:
if ((fd_destino=open(argv[2],O_WRONLY|O_TRUNC|O_CREAT|O_SYNC, 0666))==
-1)

 Lecturas predictivas
"Readahead" se refiere a la acción de leer más datos del disco y de la página de
caché en la que se hizo la petición de lectura, en efecto, leyendo un poco hacia
adelante.

 I/O usando buffers (lenguaje C estándar)


El rendimiento de I/O es óptimo cuando las peticiones se realizan entre límites
alineados por bloques y de tamaño múltiplo entero del tamaño de bloque. En la
práctica, los tamaños de bloque normalmente son 512, 1,024, 2,048, ó 4,096. En
consecuencia, la elección más sencilla es hacer operaciones de I/O usando buffers
grandes que sean múltiplos de los tamaños típicos, por ejemplo 4,096 o 8,192 bytes.

El problema, por supuesto, es que los programas rara vez efectúan estas operaciones
en términos de bloques. Los programas trabajan con campos, líneas o caracteres, no
con abstracciones como los bloques. Para remediar esta situación, los programas
pueden utilizar las funciones de usuario para I/O con buffers de usuario, declarados
por el programador. La biblioteca estándar de C provee la librería estándar de I/O
(comúnmente llamada stdio), la cual a su vez provee una solución de utilización de
buffers para usuarios, de forma independiente a la arquitectura sobre la que corra el
sistema. Estas rutinas de lectura y escritura utilizando buffers de usuarios pueden
utilizarse incluso no solo para escribir a disco, sino para escribir a cualquier dispositivo
futuro o incluso para escribir a “sockets” de red y así enviar “writes” y “reads”
provenientes de otros programas, posiblemente residentes en otras máquinas de la
red.

Que una aplicación use las funciones estándar de I/O o los accesos directos a las
llamadas de sistema es una decisión que el desarrollador debe tomar muy
cuidadosamente después de sopesar las necesidades de la aplicación y su
comportamiento.

Las funciones estándar de I/O no trabajan directamente en los descriptores de


archivos. En vez de eso, usan su propio identificador único, conocido como "apuntador
a archivo". Dentro de la biblioteca de C, el apuntador a archivo es traducido a un
descriptor de archivo.

 Apuntador a archivo
Es un apuntador al tipo de dato FILE, definido en <stdio.h>, que representa el
apuntador a archivo. Hablando del estándar I/O , un archivo abierto es llamado
"stream". Estos Streams pueden abrirse para escritura, lectura o ambos.
Para abrir un archivo se utiliza el la función fopen() y para cerrarlo fclose().

 Leyendo Streams
La biblioteca estándar de C implementa diferentes funciones para leer de un stream
abierto.

 Leyendo un caracter a la vez.


La función fgetc() se usa para leer un sólo caracter del stream.

 Leer una línea completa.


La función fgets() lee una cadena de caracteres (string) del stream.

 Leer datos binarios.


Para algunas aplicaciones, leer caracteres individuales o líneas no es suficiente.
Algunas veces, los desarrolladores quieren leer y escribir datos binarios complejos, por
ejemplo estructuras. Para esto, la biblioteca estándar I/O provee fread().

Para mayor referencia sobre estas funciones, consultar los manuales de cada función.

 Escribiendo a un stream
La biblioteca estándar de C implementa varias funciones para escribir en un stream
abierto.

 Escribiendo un carácter a la vez.


La contraparte de fgetc() es fputc():

 Escribiendo una cadena de caracteres.


La función fputs() es usada para escribir una cadena a un stream dado.

 Escribiendo una cadena de caracteres.


La función fputs() es usada para escribir una cadena de caracteres a un stream dado.

 Ejemplo de un programa que usa I/O en buffers.


Como ejemplo, se va a reescribir la función copiar.c usando buffers de I/O.

/* PROGRAMA: fcopiar.c
FORMA DE USO:
fcopiar origen destino
VALOR DE RETORNO:
0: si se ejecuta satisfactoriamente.
-1: si se da alguna condicion de error
*/
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])


{
FILE *forigen;
FILE *fdestino;
char c;

/*Analisis de los argumentos de la linea de comandos*/


if (argc != 3) {
fprintf(stderr,"Forma de uso: %s origen destino\n", argv[0]);
exit(-1);
}

/*Apertura del archivo en modo solo lectura*/


if ((forigen=fopen(argv[1],"r"))== NULL) {
perror(argv[1]);
exit(-1);
}

/* Apertura o creacion de archivos en modo solo escritura*/


if ((fdestino=fopen(argv[2],"w"))== NULL) {
perror(argv[2]);
exit(-1);
}

/* copiamos el archivo origen en el archivo destino. */


while ((fread(&c, sizeof c, 1, forigen))> 0)
fwrite(&c, sizeof c, 1, fdestino);

fclose(forigen);
fclose(fdestino);
return 0;
}

Después de compilar el archivo, comparamos el tiempo que tarda en ejecutarse el


programa fcopiar contra el programa copiar. Para reproducir estos ejemplos también se
puede usar el archivo (>1GB) que utilizaron en la práctica de System Programming.

Mapeo a Memoria (Memory map)


Los archivos mapeados a memoria son una copia idéntica en memoria de un archivo
en disco. La imagen corresponde byte por byte con el archivo en disco. Los archivos
mapeados a memoria tienen dos principales ventajas:
 Las operaciones de I/O sobre archivos mapeados a memoria evitan los buffers
del kernel, por lo tanto las transferencias I/O son mucho más rápidas. Nota: ya
que el kernel tiene un overhead, esta tasa de transferencia es más notoria con
archivos grandes.
 Aparte de los posibles fallos de página, leer de y escribir en un archivo mapeado
a memoria no incurre en ninguna llamada a sistema o sobrecarga por cambio de
contexto. Es tan simple como accesar memoria.
 Los datos del archivo mapeado pueden ser accedidos internamente mediante
apuntadores en lugar de las funciones comunes para la manipulación de
archivos. Además, buscar en el mapeo involucra manipulaciones triviales de
apuntadores. No hay necesidad de la llamada a sistema lseek().

Por estas razones, mmap() es una opción inteligente para muchas aplicaciones.

Hay un par de puntos a mantener en mente cuando se usa mmap():


 Los mapeos a memoria tienen siempre un tamaño que es múltiplo entero de
páginas. Así, la diferencia entre el tamaño del archivo y el número entero de
páginas es desperdiciado como espacio sobrado. Para archivos pequeños, un
porcentaje significativo del mapeo podría ser desperdiciado. Por ejemplo: con
páginas de 4KB, un archivo de 7 bytes mapeado desperdicia 4,089 bytes.

 Los mapeos a memoria debe ajustarse dentro del espacio de direcciones del
proceso. Con un espacio de direcciones de 32 bits, un número muy grande de
mapeos de diferentes tamaños podría resultar en la fragmentación del espacio
de direcciones, haciendo difícil encontrar grandes regiones libres contiguas. Este
problema, por supuesto, es mucho menos aparente con espacios de direcciones
de 64 bits.

 Hay un overhead al crear y mantener los mapeos a memoria y las estructuras


de datos asociadas dentro del kernel. Este overhead es obviado generalmente
por la eliminación de la doble copia mencionada en la siguiente sección,
particularmente para archivos grandes y frecuentemente accedidos.

Por estas razones, los beneficios de mmap() son extremadamente notorios cuando el
archivo mapeado es grande (y así cualquier espacio desperdiciado es un pequeño
porcentaje del mapeo total), o cuando el tamaño total del archivo mapeado es divisible
por el tamaño de página (y así no hay desperdicio de espacio).

POSIX.1 estandariza—y Linux implementa—la llamada a sistema mmap() para


mapear objetos en memoria. Una llamada a mmap() pide al kernel mapear len bytes
del objeto representado por el file descriptor fd , iniciando a partir de Offset bytes en el
archivo, en memoria. Si se incluye addr , se indica la preferencia de usar una dirección
de inicio en memoria. Los permisos de acceso son dictados por prot , y el
comportamiento adicional puede ser dado por flags (Consultar el manual de mmap).

A continuación se reescribe el programa copiar utilizando las funciones mmap() y


unmap().
/* PROGRAMA: copiar_mm.c (utilizando mapeo de memoria)
FORMA DE USO:
./copiar_mm origen destino
VALOR DE RETORNO:
0: si se ejecuta satisfactoriamente.
-1: si se da alguna condicion de error
*/
#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/mman.h>

int main(int argc, char *argv[])


{
int fd_origen;
int fd_destino;
int nbytes;
void *src;
struct stat statbuf;

/*Analisis de los argumentos de la linea de comandos*/


if (argc != 3) {
fprintf(stderr,"Forma de uso: %s origen destino\n", argv[0]);
exit(-1);
}

/*Apertura del archivo en modo solo lectura*/


if ((fd_origen=open(argv[1],O_RDONLY))== -1) {
perror(argv[1]);
exit(-1);
}

/* Apertura o creacion de archivos en modo solo escritura*/


if ((fd_destino=open(argv[2],O_WRONLY|O_TRUNC|O_CREAT, 0666))== -1) {
perror(argv[2]);
exit(-1);
}
/* Obtiene la longitud del archivo de lectura. */
fstat(fd_origen, &statbuf);

/*Mapea el archivo de entrada. */


if((src = mmap(0, statbuf.st_size, PROT_READ, MAP_SHARED, fd_origen, 0))<0)
err_quit("mmap");

nbytes = statbuf.st_size;

/* Escribe el archivo de memoria a disco. */


write(fd_destino, src, nbytes);

close(fd_origen);
munmap(src, statbuf.st_size);
close(fd_destino);
return 0;
}

Note que este programa utiliza las mismas funciones fread y fwrite para leer y escribir
archivos mapeados a memoria.

Con el propósito de comparar usaremos el archivo >1GB que se utilizó en la


práctica "System Programming", en este caso llamado 100wrds. A continuación se
presentan, en la Tabla 1, los tiempos de ejecución de 100 corridas de fcopiar y
copiar_mm, así como el tiempo promedio. También se incluyen los resultados cuando
se ejecuta el comando cp, el código que utiliza llamadas al sistema, copiar, y el
código cuando se fuerza escribir a disco duro, copiar_fw. Se puede observar que el
doble buffering puede tener efectos peores que forzar la escritura en disco.

user@localhost ~ $ time ./copiar_mm ./100wrds ./wrds

corrida fcopiar copiar_mm copiar_fw copiar cp


1 27.27 1.46 27.09 1.03 1.23
2 26.93 1.18 25.21 1.39 1.34
3 27.25 1.64 25.13 1.37 1.27
4 27.06 0.79 26.34 1.86 1.25
5 27.15 1.1 25.09 1.54 1.45
6 26.82 0.81 25.34 0.95 1.29
7 26.88 1.25 25.09 0.65 1.26
8 27.29 1.21 25.67 1 1.21
9 27.83 1.02 25.18 1.31 0.99
10 27.29 1.26 25.64 1.32 1.51
Promedio 27.177 1.172 25.578 1.242 1.28
Tabla 1. Comparación entre los tiempos de ejecución de las variantes del programa
copiar.c

Críticas a la Biblioteca I/O Estándar.


Tan ampliamente usado como es el I/O Estándar, algunos expertos apuntan a fallas en
él. Algunas de las funciones, como fgets(), son ocasionalmente inadecuadas. Otras
funciones, como gets(), son tan terribles que han sido prácticamente expulsadas de
los estándares. La queja más grande con I/O estándar es el impacto en el rendimiento
originado por la doble copia. Cuando se leen datos, el I/O estándar emite una llamada
a sistema read() para el kernel, copiando los datos del kernel al buffer I/O estándar.
Cuando una aplicación emite una petición de lectura mediante I/O estándar—por decir,
usando fget()—los datos son copiados de nuevo, en esta ocasión del buffer I/O
estándar al buffer proporcionado. Las peticiones de escritura trabajan en dirección
contraria: los datos son copiados una vez del buffer proporcionado al buffer I/O
estándar, y entonces después del buffer I/O estándar al kernel mediante write().

Una implementación alternativa podría evitar la doble copia al hacer que cada petición
de lectura regrese un apuntador al buffer I/O estándar. Los datos podrían entonces ser
leídos directamente, dentro del buffer I/O estándar, sin la necesidad de una copia
extraña. En el evento de que la aplicación quisiera que los datos se almacenarán en
su propio buffer local—quizás escribir a él—podría siempre realizar la copia
manualmente. Esta implementación proveería una interfaz “libre”, permitiendo a las
aplicaciones indicar cuando se apropian de un pedazo del buffer de lectura. Las
escrituras serían un poco más complicadas, pero aún se evitaría la doble copia.
Cuando se emite una petición de escritura, la implementación almacenaría el puntero.
Al final, cuando se está listo para vaciar los datos al kernel, la implementación podría
recorrer su lista de apuntadores almacenados, escribiendo a disco los datos. Esto
podría ser hecho usando I/O de dispersión-reunión, vía writev(), y por lo tanto solo
una llamada a sistema. Existen bibliotecas user-buffering altamente óptimas, que
resuelven el problema de la doble copia con implementaciones similares a las que se
han discutido. Alternativamente, algunos desarrolladores eligen implementar sus
propias soluciones. Pero a pesar de estas alternativas, I/O estándar permanece
popular.

Implicaciones del buffering del Estándar I/O:


 Mejores que las llamadas a sistema para peticiones pequeñas o des-
alineadas—debido al buffering de flujo.
 Cuando se leen caracteres use bibliotecas estándar.
 Cuando se lean bloques use llamadas a sistema (por la doble copia).
 El ancho de banda es limitado al copiar datos al buffer de flujo.
 Para peticiones grandes, fwrite evita el buffer.
 fread no evita el buffer.

= = = = Laboratorio = = = =
 Actividades a realizar en esta práctica se encuentran descritas en este
documento, sus respuestas deben registrarse en :

 Enlace al formulario: Práctica 7: Estrategias de I/O .

En una compañía de desarrollo de software libre se tiene un programa escrito C


denominado fusionar.c, que sirve para fusionar archivos, es decir que agregue el
contenido de un archivo al contenido de otro, y guarde el resultado en un archivo de
salida. La sintaxis para su uso debe ser la siguiente:

./fusionar <archivo1> <archivo2> <salida>

Sin embargo, por tragedias que involucran un CPU abierto durante el mantenimiento y
malas prácticas de dónde colocar un refresco de cola, el disco duro que contenía el
código se daño y solo se consiguió recuperar fragmentos del código fuente orginal. Es
su deber reescribir el código partiendo de lo que se recuperó y que se despliega en la
tabla de mas abajo. Como buen desarrollador, entregará un archivo de parche
diferencial con los cambios en las lineas, es decir, utilizará la herramienta diff. Para
indicar cuales son las líneas restauradas y su nuevo valor.

#include <stdio.h>
#include <stdlib.h>
#include <sys/____.h> /* open() */
#include <sys/____.h> /* open() */
#include <____.h> /* open() */
#include <____.h> /* read() y write() , close() */

char buffer[BUFSIZ]; /*Buffer para manipular datos. */

int main(int argc, char *argv[])


{
int fd_origen; int fd_destino; int nbytes;
int i;

/*Analisis de los argumentos de la linea de comandos*/


if (argc <= ____) {
fprintf(stderr,"Forma de uso: %s ______ \n", argv[____]);
exit(____);
}

/* Apertura o creacion de archivos en modo solo escritura. */


/* Abrir en modo solo Lectura (O_WRONLY). */
/* Si el archivo existe entonces truncar a cero bytes (O_TRUNC)*/
/* El archivo es creado en caso de que no exista (O_CREAT). */
/* El modo que se crea es 0666. */
if ((fd_destino= ____(argv[____-1], O_WRONLY|O_TRUNC|O_CREAT, 0666))==-1) {
fprintf(stderr,"Error al crear el archivo de salida \n");
perror(argv[3]);
exit(____);
}

for(____;i<3;i++) {

/*Apertura del archivo 1 en modo solo lectura (O_RDONLY). */


if ((fd_origen=open(____,O_RDONLY))== -1) {
fprintf(stderr,"Error al abrir el archivo de entrada: %s ¥n", ____);
perror(argv[1]);
exit(____);
}

/* copiamos el archivo 1 en el archivo destino. */


while ((nbytes=read(____, buffer, sizeof buffer))> 0)
write(____, buffer, nbytes);
close(____);
}

close(____);
}

 Puedes ver un ejemplo de la ejecución de este programa:

user@localhost ~$ ls
body.c fusionar.c fusionar headers.c
user@localhost ~$ cat headers.c
#include <stdio.h>
user@localhost ~$ cat body.c
const char *msg = "Hello Earthlings!";

int main ()
{
printf ("Message from Mars: %s\n", msg);

return 0;
}
user@localhost ~$ ./fusionar headers.c body.c secretProgram.c
user@localhost ~$ ls
body.c fusionar.c fusionar headers.c sec
retProgram.c
user@localhost ~$ gcc -Wall -o secretProgram secretProgram.c
user@localhost ~$ ./secretProgram
Message from Mars: Hello Earthlings!

Además de este código original se pretende recuperar archivos que se perdieron de


forma permanente los cuales tienen las siguientes características:

1. Una versión modificada de fusionar.c que fusione cualquier número de


archivos que le des como argumento, por ejemplo: que fusione todos los
archivos con extensión .c en un solo archivo:
./fusionar_todo *.c todo.c
2. Una modificación de fusionar.C que se llame fusionar_sync.c cuyo objetivo
sea forzar la escritura al disco (Synchronized I/O). Si esto les suena a lengua klingon
le sugerimos LEA LA PRÁCTICA (Como todo buen programador que primero reúne
herramientas antes de tocar código).

* Reporta los cambios realizados en cada código utilizando la herramienta diff:


diff -Naur fusionar_orig.c fusionar.c

Referencias
[1] Love, Robert. Linux System Programming, O'REILLY, 2007. Biblioteca: QA76.76.O63
L69 2007
[2] Marquez, Fernando. Programación Avanzada en UNIX, 3a. edicion, 2004. Biblioteca
QA, 76.76, .O63, M37, 2006 (Reserva)

Fuentes adicionales

 Secciones del capitulo 1 del libro de Marquez[2]: Introduccion (Temas:


Estructura del Sistema, llamadas al sistema, etapas de compilaciòn, y
Arquitectura del Sistema Operativo UNIX, Dispositivos tipo bloque y tipo
caracter, Gestion de Memoria e Interfaz de llamadas al sistema).
 Puedes leer más sobre system calls en http://en.wikipedia.org/wiki/System_call.
 Encontraras la implementación GNU de libc en:
http://www.gnu.org/software/libc/manual/html_mono/libc.html.
Hay una lista de las llamadas a sistema más comunes con el comando man syscalls
y en: http://linux.die.net/man/2/syscalls . Usaremos esta última referencia.
Laboratorio de
Sistemas Operativos
Autor: Dr. Juan A. Nolazco Flores Co-autores: M.C. Roberto Aceves, M.C. Jorge Villaseñor,
Ing. Raúl Fuentes, J.I. icaza

Práctica 8: Sistema de Archivos

Objetivo
En está práctica seguimos con los temas de desarrollo de aplicaciones en ambiente
GNU/Linux pero en esta ocasión nos enfocamos más en la definición de “Archivo”.
Como recordarán hemos hecho mucho énfasis en que en Linux “todo es un archivo”
y esto requiere especial atención cuando uno está por adentrarse con el hardware o
con otros elementos. Por lo anterior, en esta ocasión nos enfocaremos en cómo se
maneja el sistema de archivos en un S.O. GNU/Linux.

ÍNDICE
Objetivo
Tipos de Archivos
Archivos Regulares
Directorios
Enlaces (hard and soft links)
Archivos especiales
tree.c, Desplegando el tipo de archivo.
Arquitectura del Sistema Operativo
Arquitectura del sistema de archivos
Descriptores de archivos e I-Node
File descriptors.
i-Node
Estado1.c
Tablas de control de acceso a los archivos.
Copiar.c
Laboratorio
Ejercicio 1
Ejercicio 2
Ejercicio 3
Conclusiones
Referencias:
Tipos de Archivos
"Archivo" es el concepto más básico y fundamental de Linux, y no necesariamente se
refiere a lo que normalmente conocemos como “archivos en disco”. Linux trata a los
dispositivos y periféricos (terminales, teclados, unidad USB, CD roms etc.) como si
fueran archivos (everything is a file philosophy).

.System programming en Linux consiste en abrir, leer, escribir, cerrar y administrar


archivos. Cada archivo cuenta con una agarradera (handle) interior que se llama
descriptor de archivo. Ya hemos manipulado un poco estos descriptores en la primera
parte de la práctica anterior, en donde los utilizamos para hacer referencia a archivos
en disco. Pero, no se debe confundirlos con los “file pointers” que se utilizan en el
standard I/O (segunda parte de la práctica anterior)

El descriptor de archivo es un entero utilizado dentro del Núcleo (Kernel) para


referenciar un archivo. Estos descriptores de archivo se comparten con el espacio de
usuario, de tal forma que son utilizados directamente por los programas usuario para
accesar archivos.

Archivos Regulares
Lo que la mayoría de nosotros llamamos “archivo” es lo que linux etiqueta como
archivos regulares. Un archivo regular contiene bytes de datos, organizados en un
arreglo lineal llamado flujo de bytes (byte stream). Ejemplos de estos son los archivos
de texto, documentos generados por alguna suite de oficina, archivos ejecutables, etc.

Directorios
Actúan como un contenedor para otros archivos y directorios; en otros ambientes
suelen llamarse “folders”. Un directorio es en sí un archivo, que contiene una lista de
nombres de otros archivos o directorios contenidos dentro del directorio,. Cada uno de
estos nombres está a su vez asociado con un número de nodo-i (i-node), que es un
identificador del archivo interno al kernel.

 Los directorios son los archivos que nos permiten darle una estructura
jerárquica a los sistemas de archivos de Linux; por ejemplo el sistema o
conjunto de archivos que reside en una partición de un disco duro o un USB.

 La función fundamental de un directorio consiste en establecer la relación que


existe entre el nombre de cada archivo contenido dentro del directorio, y el
número de nodo-i correspondiente al archivo

 Los directorios residen como archivos ordinarios dentro del sistema de


archivos.
o Pueden ser leídos como archivos ordinarios.
o No se pueden crear o escribir sobre ellos como archivos ordinarios (el
kernel los protege por razones de seguridad).

La información de un directorio puede obtenerse a través de la llamada al sistema:


readdir(), o la llamada al sistema stat() para investigar si se trata de un subdirectorio
dentro de un directorio.

Enlaces (hard and soft links)


Un enlace o link es un nombre dentro de un directorio, que internamente apunta a
un nodo-i que puede corresponder a un archivo en el mismo o en otro directorio. En
otros ambientes suelen denominarse shortcuts. La diferencia entre un enlace hard y un
enlace soft es que el enlace hard no puede cruzar (se invalida) entre diferentes
sistemas de archivos (por ejemplo entre el sistema de archivos del disco duro y el
sistema de archivos de un USB). En cambio, un enlace soft puede apuntar a cualquier
lado, incluyendo archivos y directorios que residen en diferentes sistemas de archivos.

Archivos especiales
 Archivos de dispositivos basados en caracteres, como el “archivo” teclado,
 Archivos de dispositivos basados en bloques, como los archivos en disco.
 En los dispositivos de modo bloque hay un buffer que mejora enormemente la
velocidad de transferencia. Un mismo dispositivo puede ser accedido de modo
bloque o de modo carácter dependiendo del “driver” utilizado.
 Pipes con nombre, que como sabemos se utilizan para ligar la salida (standard
out) de un programa con la entrada (standard in) de otro y
 Sockets, que se utilizan para inter-comunicar programas que pueden residir en
la misma o diferentes máquinas.

tree.c, Desplegando el tipo de archivo.


/*** PROGRAMA:tree.c
FORMA DE USO:
Tree [opciones] <nombre_directorio> [ <nombre_directorio> ... ]
(opciones)
-f mostrar los archivos que hay dentro de
Un directorio agregando:
(d) directorio
(o) archivo ordinario
(b) archivo especial modo bloque
(c) archivo especial modo caracter
(p) fifo
(x) archivo ejecutable
VALOR DE RETORNO:
0: si se ejecuta correctamente.
-1: si se produce algun error. ***/

#include <stdio.h>
#include <stdlib.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <dirent.h>
#include <string.h>
struct opciones
{
unsigned mostrar_archivos:1;
};

enum Boolean {NO,SI};


void tree(char *path, struct opciones opciones);

/*** FUNCION: main


DESCRIPCION
Funcion principal , se encarga de analizar los argumentos
De la linea de ordenes y de invocar a la funcion tree.***/
int main (int argc, char *argv[])
{
struct opciones opciones;
int i, j;

/*Analisis de los argumentos en la linea de ordenes.*/


if (argc<2)
{
fprintf (stderr,"Forma de uso: tree [-f] nombre_directorio\n");
exit (-1);
}

for (i=1;i<argc;i++)
if (argv[i][0] =='-')
for (j=1;argv[i][j]!=0;j++)
switch (argv[i][j])
{
case 'f':
opciones.mostrar_archivos= SI;
break;
default:
fprintf(stderr, "Opcion [-%c] desconocida \n",argv[i][j]);
}

/*Analisis de la estructura en arbol de directorios para cada uno


de los argumentos que aparecen en la linea de ordenes y que no
son opciones de tree.*/
for (i=1;i<argc;i++)
if (argv [i][0]!='-')
tree (argv[i],opciones);

return 0;
}

/*** FUNCION: tree


DESCRIPCION:
Esta funcion recibe el path name de un directorio se encarga de
analizarlo. Si en el directorio hay subdirectorios, la
funcion se
llama de forma recursiva para analizar ese subdirectorio.
Segun las opciones que esten activas, la función desplegara por
pantalla un tipo de informacion u otra.
***/
void tree(char *path, struct opciones opciones)
{
DIR *dirp;
struct dirent *dp;
static unsigned int nivel = 0;
struct stat buf;
int ok;
int i;
char archivo [256];
char tipo_archivo;

/*Apertura de directorio.*/
if ((dirp=opendir(path)) == NULL)
{
perror(path);
return;
};

/*Leemos, una por una, las restantes entradas del directorio.*/


while ((dp=readdir(dirp))!=NULL)
{
/*Formamos el path name correspondiente al archivo de
la entrada de
directorio que estamos procesando*/
sprintf(archivo,"%s/%s",path, dp->d_name);

/*Lectura del inode del archivo. */


ok=stat(archivo,&buf);

/*Si el archivo es un subdirectorio, llamamos nuevamente a tree.*/


if (ok!=-1 && (buf.st_mode & S_IFMT) == S_IFDIR)
{
for (i=0;i<nivel;i++)
printf("\t");
printf ("%s %s \n",dp->d_name, opciones.mostrar_archivos ? "(d)":"");

/* Si es referencia a "este directorio" (.) o el "directiorio padre"


(..), saltar) */
if (!strcmp(".", dp->d_name) || !strcmp("..", dp->d_name))
continue;

++nivel;
tree(archivo,opciones);
--nivel;
}
/* Si el archivo no es un directorio y esta activa la opcion
Mostrar_archivos (-f), presentamos por pantalla el
nombre del
archivo y su tipo.*/
else {
if (ok !=-1 && opciones.mostrar_archivos== SI) {
for (i=0;i<nivel;i++)
printf ("\t");
switch (buf.st_mode & S_IFMT)
{
case S_IFREG:
if (buf.st_mode & 0111)
tipo_archivo = 'x';
else
tipo_archivo = 'o';
break;
case S_IFCHR:
tipo_archivo = 'c';
break;
case S_IFBLK:
tipo_archivo = 'b';
break;
case S_IFIFO:
tipo_archivo = 'p';
break;
default:
tipo_archivo = '?';
} //switch
fprintf(stdout,"(%c) %s \n", tipo_archivo, dp->d_name);
}else
fprintf(stdout," %s \n", dp->d_name);
}
} /* while */
closedir(dirp);
} /* tree */

Primero se compila para obtener el programa ejecutable utilizando el siguiente


comando:
[user@localhost home]$ gcc –o tree tree.c

Después, se ejecuta como si fuera un comando:

[user@localhost home]$ ./tree hola



 hola1
hola11
hola12

 hola2

[user@localhost home]$ ./tree -f hola

 hola1 (d)
(o) h1
hola11 (d)
hola12 (d)

hola2 (d)
(o) h2

(o) h

Du - estimate file space usage


El comando du (disk ussage) nos dice cuánto espacio (en bloques) utiliza cada
directorio. Si deseas verlo en formato entendible por el humano usa la opción -h .

[user@localhost home]$ du
22 ./Finales/al149830
18 ./Finales/al246793
14 ./Finales/al158072
170 ./Finales/al171574
10 ./Finales/al171465
50 ./Finales/al501240
16 ./Finales/al501747
16 ./Finales/al177269
320 ./Finales
72 ./CyUnixAvanzado
10 ./pruebas
402 .

El valor mostrado indica cuántos bloques poseen los archivos en el directorio indicado
en la línea de comandos del du (default: el directorio local) incluyendo subdirectorios y
el mismo directorio (en el ejemplo, el directorio local ocupa 402 bloques incluyendo
todos sus subdirectorios y archivos).

Los bloques son importantes peus son la referencia directa a los clusters del disco
duro donde se almacenan (o los bloques en memorias flash), sin embargo, nosotros
estamos acostumbrados a ver el tamaño en términos de Bytes y es posible lograrlo
con el argumento -h.

Arquitectura del Sistema Operativo


Linux es propiamente el Kernel de un sistema UNIX; por lo mismo tiene una
arquitectura heredada de Unix y de la cual mantiene su sencillez. Es una arquitectura
de capas que posee 3 elementos:
 Capa de hardware
 Capa de Kernel
 Capa de Usuario

Al ser un sistema jerárquico y de capas, toda la comunicación hacia el hardware debe


de pasar por la capa de Kernel (y es en este punto donde esta el control y la
seguridad). La siguiente imagen ayudara a su apreciación:

Una distribución de linux o distro, tal como Ubuntu o Fedora, agrega al Kernel un
shell, un conjunto de comandos, una interfase tipo GUI y varias aplicaciones útiles;
pero todas las distribuciones utilizan básicamente el mismo Kernel, que ha sido
desarrollado y mantenido durante muchos años por una extensa comunidad
encabezada por Linus Torvald.

Durante las prácticas pasadas ya hemos estudiado el manejo de las librerías de GNU
ya que se trata de las herramientas proveídas en cualquier distro basado en Linux
(Ubuntu, Fedora, Red-hat , Arch, etc...) y se trata de comandos como cd, awk, ls, rm,
tar, rsync, etc... que pueden formar parte de shell scripts. Cuando usamos cualquier
comando tal como “rm -f /”, el Kernel valída primero los privilegios y permisos del
usuario que lo manda a ejecutar antes de realizar la acción. Con el comando mostrado
en particular, seguramente no lo realizará pues a excepción de Root nadie tiene
control del directorio raíz (Por cierto, ese comando tiende a ser una broma pesada en
los foros de Linux).
Además ya hemos manipulado la interacción con el hardware al crear programas que
copian archivos dentro del disco duro, ya sea directamente con los comandos read y
write o utilizando librerías de usuario (standard I/O).En ambos casos, es el Kernel
quien toma el control., sin embargo a momentos distintos, esto se puede apreciar con
la siguiente imagen donde se vuelven a marcar las 3 capas pero desplegando más
ampliamente las acciones que se realizan:

La diferencia entre los dos radica completamente en lo que se vio en la práctica


anterior referente a I/O System Programming.

Arquitectura del sistema de archivos


Linux proporciona un espacio de nombres de archivos y directorios global (aplicable a
múltiples dispositivos) y unificado (el mismo para diferentes dispositivos). En un
espacio de nombres unificado:
 El acceso a los archivos en discos o unidades diferentes se realiza siempre
dentro de un espacio de nombres unificado, es decir, todos los archivos son
accesibles dentro de la misma y única jerarquía de directorios.
 Un archivo en un disco podría ser accesible mediante la ruta
/media/usbdisk/plank.jpg, o incluso mediante /home/desktop/plank.jpg (i-
nodes), esto mediante ligas (Algo parecido a los atajos de Windows).

En un espacio de nombres no unificado:


 Los archivos contenidos en diferentes discos o unidades son accesibles a
través de diferentes espacios de nombres, es decir, hay un espacio de
nombres diferente para cada disco.
 En Winows, un archivo en una memoria USB podría ser accesible mediante la
ruta F:\plank.jpg, mientras que en el disco duro está ubicado en C:\; en Linux
en cambio, ambos tipos de archivo se accedería con un “path” del mismo tipo,
que podría ser por ejemplo /media/usbdisp/palnk.jpg para el caso del usb o
/home/user/fulano/plank.jpg. Lo que ha sucedido es que el sistema de archivos
completo del USB se ha “montado” en usbdisp, y entonces se hace visible
como un directorio cualquiera.

Un sistema de archivos:
 Es una colección de archivos y directorios en una jerarquía válida y formal.
 Los sistemas de archivos pueden ser agregados y removidos individualmente
del espacio de nombres global de archivos y directorios.
 Linux también soporta sistemas de archivos virtuales que existen sólo en
memoria, y sistemas de archivos de red que existen en máquinas a través de la
red.
 Los sistemas de archivos físicos residen en dispositivos de almacenamiento en
bloque, como CDs, unidades flash, tarjetas compact flash, o discos duros.
Algunos de estos dispositivos son divisibles en particiones, lo que significa que
pueden ser divididos en múltiples sistemas de archivos, los cuales pueden ser
manipulados individualmente.

La unidad direccionable más pequeña en un dispositivo de bloque es el sector.


El sector es una característica física del dispositivo. El tamaño de los sectores es
casi siempre una potencia de dos;por ejemplo, 512 bytes es muy común. Un
dispositivo de bloque no puede transferir o accesar una unidad de datos más
pequeña que un sector; todas las operaciones de I/O ocurren en términos de uno o
más sectores.
Igualmente, la unidad lógica direccionable más pequeña en un sistema de
archivos es el bloque.
El bloque es una abstracción del sistema de archivos, no del medio físico donde el
sistema de archivos reside. Un bloque es usualmente un múltiplo del tamaño de
sector. Los bloques son generalmente más grandes que el sector, pero deben ser
menores que el tamaño de página (la unidad más pequeña direccionable por la
unidad de memoria, un componente del hardware). Tamaños comunes de bloque
son 512 bytes, 1 kilobyte y 4 kilobytes.

Descriptores de archivos e I-Node

File descriptors.
Antes de que un archivo pueda ser leído o escrito, debe ser abierto. El kernel mantiene
una lista de archivos abiertos por proceso, llamada la tabla de archivos. Esta tabla es
indexada a través de enteros no negativos conocidos como file descriptors
(comúnmente abreviado fd). Cada entrada en la lista contiene información sobre un
archivo abierto, incluyendo un apuntador a una copia en memoria del i-node del
archivo y meta-datos asociados, como son la posición del archivo y los modos de
acceso. Tanto el espacio de usuario como el espacio de kernel usan file descriptors
como cookies únicas por proceso. Abrir un archivo regresa un file descriptor, mientras
las operaciones subsecuentes (leer, escribir, y así sucesivamente) toman el file
descriptor como su argumento primario.

i-Node
Un archivo posee varios componentes:
 nombre,
 contenido e
 información administrativa (permisos y fechas de modificación).

Aunque los archivos son usualmente accedidos mediante nombres de archivo, en


realidad no están asociados directamente con esos nombres. En cambio, un archivo
es referenciado por un i-node (originalmente information node), al cual se le asigna
un valor numérico único. Este valor se llama i-node number, a menudo abreviado
como i-number o ino. Un i-node almacena meta-datos asociados con un archivo, como
su fecha de modificación, propietario, tipo, longitud, y la localización de los datos del
archivo –¡pero no el nombre del archivo! El i-node es tanto un objeto físico, ubicado en
el disco en un sistema de archivos tipo Unix, como una unidad conceptual,
representada por una estructura de datos en el kernel de Linux. La relación de i-nodes
con nombres de archivo la mantiene el directorio, como veíamos antes.

Cada archivo tiene asociado un “nodo-i”. El i-node tiene información necesaria para
que un proceso pueda acceder a los archivos. Los campos de un i-node son:
 Tipo de archivo: ordinario, directorio, especial o tubería (pipe).
 Tipo de acceso: permisos de usuario, grupo y resto de usuarios.
 Tiempos de acceso al archivo: fecha de última modificación, fecha de la última
vez que se accedió al archivo, y de la última vez que se modificó.
 Tamaño del archivo.
 Apuntadores a bloques de disco donde se almacena el contenido del archivo:
los archivos no tienen porque estar físicamente en bloques continuos.

Nota: el nodo-i NO contiene el nombre del archivo. Es en los archivos


directorios es donde a cada i-node se le asigna un nombre de archivo.

El tamaño de un i-node es fijo. Con el propósito de direccionar archivos muy grandes


(recordemos que un archivo no está en bloques continuos), entonces en UNIX sistema
V, los nodos-i tienen una tabla de tamaño 13. Las primeras 10 posiciones de esta
tabla, entradas directas, contienen direcciones de bloques con datos de archivos. La
entrada 11, entrada indirecta simple, apunta a un bloque que contiene una tabla de
direcciones de bloques, que apuntan al bloque de datos. La posición 12, entrada
indirecta doble, apunta a un bloque que contiene una tabla de direcciones que apunta
a bloques que contienen direcciones donde se encuentran los datos. La posición 13 es
una entrada indirecta triple, como se muestra en la siguiente figura:

El número de bloques direccionables es entonces:


Un bloque puede ser de tamaño 1024, 4096, etc.

El SO carga la lista de nodos-i del disco a memoria principal, conocida como tabla de
nodos-i.

La tabla de nodos-i contiene la información de la lista de i-nodes más otros datos como
estado el estado de cada nodo-i (p. ej., si el nodo-i esta bloqueado, si hay algún
proceso que esté esperando que el nodo-i se desbloquee, si la copia del inode que
hay en memoria es diferente a la copia de disco, etc.)

Importante: el sistema de archivos manipula (cambio de permisos, propietario del


archivo, etc.) la tabla de nodos-i, pero no modifica directamente la lista de nodos-i en
el disco en el instante en que ocurren estos cambios. Esto podría plantear problemas
de inconsistencia entre la tabla y la lista de nodos-i, ya que una actualización de la
tabla de nodos-i, no implica una actualización inmediata de la lista de nodos-i. La
solución a esta inconsistencia es que el sistema realiza actualizaciones periódicas de
la lista de nodos-i. Naturalmente, antes de apagar el sistema también es necesario
hacer una actualización de la lista de nodos-i. El programa shutdown es el encargado
de esta tarea.

La información de los nodos-i de archivos y directorios puede ser accesible a través de


llamadas al comando stat().

Estado1.c
A continuación se presenta el programa estado1.c, que muestra el tipo de un archivo
basándose en la información contenida en su nodo-i:
/* PROGRAMA: estado1.c (basado en programa 3.10 en tercera edicion.)
DESCRIPCION:
Presenta por pantalla la informacion estadistica de
nombre_archivo.
FORMA DE USO:
estado <nombre_archivo> [<nombre_archivo> ...]
VALOR DE RETORNO:
0: si se ejecuta satisfactoriamente.
-1: si se da alguna condicion de error.
*/
#include <stdio.h>
#include <stdlib.h>
#include <pwd.h>
#include <grp.h>
#include <sys/types.h>
#include <sys/stat.h>

char permisos[]={'x','y','r'};

int estado(char *archivo)


{
struct stat buf;
struct passwd *pw;
struct group *gr;
int i;

if (stat(archivo, &buf)== -1) /* Lee informacion del tipo archivo. */


{
perror(archivo);
exit(-1);
}

fprintf(stdout," Archivo: %s\n", archivo);


fprintf(stdout," Nro. de inode: %d\n", buf.st_ino);

fprintf(stdout," Tipo:");
/* Analisis del tipo de dispositivo*/
/* Para concer el tipo de archivo, se utilizan unas constantes
definidas*/
/* en sys/stat.h*/
/* S_IFMT= 0170000 */
/* S_IFREG=0100000 */
/* S_IFDIR=0040000 */
/* S_IFCHR=0020000 */
/* S_IFBLK=0060000 */
/* S_IFIFO=0010000 */
switch (buf.st_mode & S_IFMT) /* mascara para obtener el tipo de
archivo*/
{
case S_IFREG:
printf("ordinario\n");
break;
case S_IFDIR:
printf("directorio\n");
break;
case S_IFCHR:
printf("especial modo de caracter\n");
break;
case S_IFBLK:
printf("especial modo bloque\n");
break;
case S_IFIFO:
printf("FIFO");
break;
}
}

int main(int argc, char *argv[])


{
int i;

/*Analisis de los argumentos de la linea de comandos. */


fprintf(stderr,"argc=%d\n",argc);
if (argc < 2)
{
fprintf(stderr,"Forma de uso: %s nombre_archivo\n", argv[0]);
exit(-1);
}

/* Ciclo de conteo */
for (i=1; i<argc; i++)
{
estado(argv[i]);
}
exit(0);
return 0;
}

Primero se compila para obtener el programa ejecutable utilizando el siguiente comando:


[username@localhost home]$ gcc –o estado1 estado1.c

Después se ejecuta como si fuera un comando:

[username@localhost home]$ ls
copiar.c hola
[username@localhost home]$ ./estado1 copiar.c
Archivo: copiar.c
Nro. de inode: 20578307
Tipo:ordinario
[username@localhost home]$ ./estado1 hola
Archivo: hola
Nro. de inode: 20578313
Tipo:directorio
Otra forma de ver los nodos-i de los archivos es utilizando la opción -i del comando ls
como se muestra a continuación:
[username@localhost home]$ ls -i
1145832168 alphawords 1145831648 alphawords~
1145832166 fruits

Tablas de control de acceso a los archivos.

Además de la tabla de i-nodes, el Kernel mantiene en memoria otras dos tablas con
información necesaria para manipular un archivo: tabla de archivos y la tabla de
descriptores de archivo.

La tabla de archivos es una estructura global del kernel y en ella hay una entrada
para cada archivo que los procesos del kernel o del usuario tienen abiertos.

La tabla de descriptores de archivos es una estructura local a cada proceso. Esta


tabla identifica todos los archivos abiertos por proceso. En cada una de las entradas
de la tabla hay un apuntador a una entrada de la tabla de archivos del sistema.
Cuando arranca un proceso, el sistema abre automáticamente tres archivos (0,
entrada estándar; 1, salida estándar; 2, error estándar).

Cuando un proceso invoca una llamada (open, creat, dup, link) para hacer una
operación sobre un archivo, le va a pasar al kernel el descriptor de archivos. El kernel
va a utilizar ese número para acceder a la tabla de archivos del proceso, y buscar en
ella cual es la entrada de la tabla de archivos que le da acceso a su nodo-i. A través
del nodo-i es que se tiene acceso a los datos. Si el usuario crea otros archivos,
entonces el número de descriptor de archivo continua a partir de este numero.

Copiar.c

/* PROGRAMA: copiar.c
FORMA DE USO:
./copiar origen destino
VALOR DE RETORNO:
0: si se ejecuta satisfactoriamente.
-1: si se da alguna condicion de error
*/
#include <stdio.h>
#include <stdlib.h>
#include <fcntl.h>

char buffer[BUFSIZ];

int main(int argc, char *argv[])


{
int fd_origen;
int fd_destino;
int nbytes;

/*Analisis de los argumentos de la linea de comandos*/


if (argc != 3)
{
fprintf(stderr,"Forma de uso: %s origen destino\n",
argv[0]);
exit(-1);
}

/*Apertura del archivo en modo solo lectura*/


if ((fd_origen=open(argv[1],O_RDONLY))== -1)
{
perror(argv[1]);
exit(-1);
}

/* Apertura o creacion de archivos en modo solo escritura*/


if ((fd_destino=open(argv[2],O_WRONLY|O_TRUNC|O_CREAT, 0666))== -1)
{
perror(argv[2]);
exit(-1);
}
fprintf(stdout,"Num. descriptor del origen %d\n", fd_origen);
fprintf(stdout,"Num. descriptor del destino %d\n", fd_destino);

/* copiamos el archivo origen en el archivo destino. */


while ((nbytes=read(fd_origen, buffer, sizeof buffer))> 0)
write(fd_destino, buffer, nbytes);

close(fd_origen);
close(fd_destino);
return 0;
}

al ejecutralo generaria:

[username@localhost home]$ ./copiar original.txt copia.txt


Num. descriptor del origen 3
Num. descriptor del destino 4

Dos archivos pueden tener asignado el mismo nodo-i. El comando utilizado para asignar
ligas(hard links o soft links) a nombres de archivo es “ln”.

[username@localhost home]$ ln tmp tmpl


[username@localhost home]$ ls –il
total 60
20578307 -rw-r--r-- 1 username username 1158 Aug 27 15:45 copiar.c
20578323 -rw-r--r-- 1 username username 2263 Aug 28 21:05 entrada.c
20578312 -rw-r--r-- 1 username username 2155 Aug 27 20:30 estado.c
20578310 -rw-r--r-- 1 username username 3265 Aug 27 19:45 fcntl.c
20578313 drwxr-xr-x 4 username username 4096 Aug 28 19:51 hola/
20578311 -rw-r--r-- 2 username username 4 Aug 27 19:42 tmp
20578311 -rw-r--r-- 2 username username 4 Aug 27 19:42 tmpl
20578322 -rw-r--r-- 1 username username 4363 Aug 28 20:35 tree.c

El comando rm en realidad no borra nodos-i; borra entradas del directorio a nodos-i. Solo
cuando la última liga a un archivo desaparece, entonces el sistema borra el nodo-i, y en
consecuencia, también el archivo.

[username@localhost home]$ rm tmp


rm: remove regular file `tmp'? y
[username@localhost home]$ ls –il
total 56
20578307 -rw-r--r-- 1 username username 1158 Aug 27 15:45 copiar.c
20578323 -rw-r--r-- 1 username username 2263 Aug 28 21:05 entrada.c
20578312 -rw-r--r-- 1 username username 2155 Aug 27 20:30 estado.c
20578310 -rw-r--r-- 1 username username 3265 Aug 27 19:45 fcntl.c
20578313 drwxr-xr-x 4 username username 4096 Aug 28 19:51 hola/
20578311 -rw-r--r-- 1 username username 4 Aug 27 19:42 tmpl
20578322 -rw-r--r-- 1 username username 4363 Aug 28 20:35 tree.c
[username@localhost home]$

El propósito de una liga es de dar dos nombres a un mismo archivo, para que éste
pueda aparecer en dos directorios diferentes.

Puede haber ligas suaves o duras. La liga suave se utiliza cuando se hace un ligado a
un archivo que se encuentra en un sistema de archivos diferente. El ligado también
puede hacerse entre directorios.

--- Ejercicios ---


Para el desarrollo de esta práctica manejaremos cierta cantidad de archivos,
para preparar un directorio temporal ejecute el siguiente Shell scripting (Descargará
archivos exclusivamente de la Página del laboratorio).

Ejercicio 1
Cree el directorio en ~/SistArchivos/ , guarde en dicho directorio el siguiente shell
scripting con el nombre “preparacion.sh” y ejecutelo:
#!/bin/bash
mkdir originales respaldo
mkdir originales/Nivel
mkdir originales/Nivel/Segundo
cat /etc/passwd > originales/Listado
wget http://cs.mty.itesm.mx/lab/operativos/img_tec/thumb_3.png
wget http://cs.mty.itesm.mx/lab/operativos/Complementos/Practica7.tar
mv thumb_3.png originales/thumb_3.png
mv Practica7.tar originales/Practica7.tar
ls / -R > originales/Nivel/Segundo/Listado.txt
ln originales/Listado respaldo/Arch1
ln -s ../originales/thumb_3.png respaldo/Arch2
ln originales/thumb_3.png respaldo/Arch3

Ejercicio 2
Guarde el archivo tree.c en ~/SistArchivos/ y a continuación compílelo (nombre de
salida: Tree) para después ejecutarlo utilizando como directorio objetivo el directorio
~/SistArchivos/ de tal forma que despliegue la información de cada archivo.

1. Despliegue la ejecución del programa


2. ¿Por qué el directorio originales/Nivel muestra 3 directorios?
3. ¿Por que si preparacion.sh es un archivo de texto al igual que
originales/Listado, el,primero aparece con la leyenda (x) en vez de (o)?
4. ¿Por qué tree.c y Tree son diferentes para el S.O.?
5. ¿Como identifica a los archivos en respaldo/ ? ¿De qué se diferencian respecto
a /originales? (Utilice el shell del Ejercicio 1 como referencia)

Ejercicio 3
Guarde el archivo estado1.c en ~/SistArchivos/ y a continuación compilelo (nombre de
salida: Estado1) para después ejecutarlo utilizando como argumento
~/SistArchivos/originales/

1. Despliegue la ejecución del programa


2. ¿Que representa lo que regresó?
3. Ejecute el programa Estado1 en los archivos ordinarios en
~/SistArchivos/originales/ y los archivos ordinarios en
~/SistArchivos/respaldo. Despliegue las salidas (los utilizara en el siguiente
ejercicio).

Ejercicio 4
Guarde el archivo copiar.c en ~/SistArchivos/ y a continuación compílelo (nombre de
salida: Copiar)

Ejecute el siguiente comando

rm ~/SistArchivos/originales -f -R

1. ¿Por qué es correcta la afirmación de que se puede recuperar cierta


información?
2. ¿Que archivo ordinario no se puede recuperar (Utilice de referencia el ejercicio
1)?
3. Compruebe sus respuestas anteriores mediante los I-nodes que obtuvo en el
ejercicio anterior.
4. Si revisa el shell scripting notará que Arch2 y Arch3 fueron creados del mismo
archivo. ¿Que ocurre con Arch2 ahora?
5. Ejecute copiar cuyos argumentos seran ~/SistArchivos/respaldo/arch1
~/SistArchivos/respaldo/arch4
6. ¿Que diferencia a arch1 y arch4 para el S.O.? ¿Como lo puede
comprobar?

Conclusiones
Mencione sus conclusiones personales respecto a sistemas de archivos en
GNU/Linux.

Laboratorio
Acceso directo a la liga de formulario: SISTEMA DE ARCHIVOS

Referencias:
Love, Robert., Linux System Programming, O'REILLY, 2007. Biblioteca: QA76.76.O63 L69
2007
Marquez , Fernando, Programación Avanzada en UNIX, 3a. edicion, 2004. Biblioteca QA,
76.76, .O63, M37, 2006 (Reserva)

Complement

INSTITUTO TECNOLÓGICO Y DE ESTUDIOS


SUPERIORES DE MONTERREY

LABORATORIO DE SISTEMAS OPERATIVOS

Práctica

Manejo de dispositivos desde el kernel


I. Dispositivos.
i. Ejemplo
II. Ratón
III. Referencias:
IV. Laboratorio:

Dispositivos.

Los dispositivos engloban todos los periféricos que se conectan a la computadora.

Los archivos de dispositivos están en el directorio /dev. Igual que un archivo


ordinario, se utilizan las llamadas al sistema open y close para abrir y cerrar un
dispositivo.

Los datos que maneja un dispositivo dependerán de su naturaleza, y será


responsabilidad del programador del sistema operativo y sus “drivers” interpretar
qué es lo que está leyendo del dispositivo, o de enviarle datos que le hagan sentido
--por ejemplo, no tendría sentido enviar una imagen a un teclado. Dentro del
núcleo (kernel) las referencias a este archivo/dispositivo se pasan directamente a
su controlador (device driver) el cual convierte estas peticiones de archivo en
comandos al hardware del dispositivo.

Existen dispositivos orientados a transmisión por bloque (b), orientados a


transmisión por carácter (c) y orientados a transmisión de red, quedando estos
últimos fuera del enfoque de esta práctica. La lista de dispositivo se puede obtener
utilizando el comando ls, con la opción –l sobre el directorio de dispositivos:
[user@localhost home]$ ls -l /dev
total 0
brw-rw---- 1 root root 10, 6 Jun 22 16:02 hda1 /*hard disk */
crw-rw---- 1 root root 10, 6 Jun 22 16:02 sda1 /* hard disk */
crw--w--w- 1 root root 15, 0 Aug 15 20:29 tty0 /* terminal */
brw-rw---- 1 root disk 8, 0 Sep 10 19:26 /dev/sda2 /*Flash*/


Como se dijo, los dispositivos se manejan como archivos; un ejemplo es el teclado.
El teclado es un dispositivo orientado a transferencia por caracter. El disco
duro es un ejemplo de dispositivo orientado a bloque. Los dispositivos orientados
a bloque son aquellos que necesitan mover una gran cantidad de datos en una sola
operación.

Por ejemplo, observe en el siguiente comando que requiere datos del teclado:
[user@localhost home]$ cat > tmpf
Hola
Este es un ejemplo
CTRL+D

que mientras esté en el mismo renglón (datos en el buffer) puede regresare con
backspace; pero una vez oprime RETURN (se vacía el buffer), ya no puede regresar
al renglón anterior (ya que la información ya no está en el buffer).

Por otro lado, un dispositivo orientado a transferencia por caracter no tiene


buffer, por ejemplo una terminal virtual (consola). Tan pronto recibe un caracter lo
despliega.

Con las llamadas al sistema ioctl() es posible cambiar las características de los
dispositivos. Como se dijo anteriormente el teclado es un dispositivo orientado a
transmisión por caracter. Utilizando la llamada al sistema ioctl, podemos cambiar
de modo de transferencia de los datos por caracter al modo de transferencia por
buffer. A continuación se presenta el código que hace esta tarea:

La función ioctl() provee un mecanismo de bajo nivel para acceder a las


características de los dispositivos del sistema. La forma de llamada es:

ioctl(FILEHANDLE, REQUEST, ARG )

El primer argumento es el “file descriptor” del archivo/dispositivo; el


segundo especifica qué operación es la que se solicita y el tercero contiene
los parámetros con los que vamos a programar el dispositivo. Si la
operación es de lectura del estado del dispositivo, ARG contendrá los
valores con los que ya está programado.

Tanto REQUEST como ARG dependen del tipo de dispositivo que queremos
controlar.

Ejemplo
El siguiente programa muestra la forma de uso de la llamada al sistema ioctl(). Al
ejecutarlo, suprime el ECO del teclado en la consola (no muestra las teclas que se
presionan) y no espera la tecla ENTER para leer los datos del teclado:

/***
PROGRAMA; entrada.c
DESCRIPCION: Programa de ejemplo para ilustrar el empleo de
ioctl sobre terminales.
***/
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <termio.h>
#define BS 127 /* Back space. Normalmente tiene asociado el ascii 8.
*/

/***
FUNCION: leer_cadena
DESCRIPCION: Esta funcion lee de la entrada estandar una cadena
cuya longitud máxima no debe superar los n caracteres.
***/
char *leer_cadena(int n)
{
char *buffer, c;
int i = 0;
struct termio parametros_ant;
struct termio parametros;

/* Lectura de los parámetros actuales del archivo estándar de


entrada.*/
ioctl (0, TCGETA, &parametros_ant);
/*Programacion del archivo estandar de entrada con nuevos
parametros.*/
parametros = parametros_ant;
/* Suprimimos la entrada en modo canonico y el eco local. En modo no
canonico NO espera el ENTER para leer el teclado. En modo NO eco, no
se ve lo que se imprime en el recado.*/
parametros.c_lflag &= ~(ICANON|ECHO);
parametros.c_cc[4] = 1; /* Devolver el control despues
de leer un
caracter. */
ioctl (0, TCSETA, &parametros);
/* Reserva de memoria para cadena de caracteres. */
if ((buffer = (char *)malloc(sizeof(char)*(n+1))) == NULL)
return NULL;

/* Lectura de la entrada estandar. */


while (i<n && c != '\n') {
read(0, &c, 1);
/* Tratamiento del caracter de retroceso. Este caracter provoca
que se borre, tanto del buffer como de la pantalla, el
ultimo
carácter leido. */
if (c == BS || c == 8) {
if (i > 0) {
--i;
putchar ('\b');
putchar (' ');
putchar ('\b');
}
} else {
buffer[i++] = c;
putchar(c);
}
fflush (stdout);
}

buffer[i]=0;/* Caracter de final de string. */

/* Restauracion de los parametros del fichero estandar de


entrada. */
ioctl (0, TCSETA, &parametros_ant);

return (buffer);
}
main()
{
char *str;

int longitud;
char *leer_cadena ();

printf ("Longitud maxima de la cadena a leer: ");


scanf ("%d",&longitud);

str = leer_cadena(longitud);
printf ("\n%s \n",str);
}

A continuaciòn un ejemplo de su ejeccuciòn


[user@localhost home]$ ./entrada
Longitud maxima de la cadena a leer: 5
13445
13445

Con las opciones (ECHO), seria la misma salida pero pero lo que vamos
escribiendo en el teclado no aparece en pantalla hasta que oprimimos
<RETURN>.

Con las opciones (ICANON):


[user@localhost home]$ ./entrada
Longitud maxima de la cadena a leer: 5
1122334455
12345

se puede observar que hay echo el presionar una tecla pero en realidad solo
se lee un dato.

Para los archivos de dispositivos se utilizan un par de números:


'mayor' es el que codifica el tipo de dispositivo, y
'menor' es el que distingue casos diferentes del dispositivo (i.e.
diferente puerto)

Ratón
Otro dispositivo del que podemos obtener información fácilmente es el ratón. Este
dispositivo es accesible mediante la ruta /dev/input/mice . Si el archivo mice no te
entrega datos puedes intentar con mouse0, mouse1, etc.

El ratón es un dispositivo de tipo caracter. Los datos que entrega este dispositivo
tiene el siguiente formato:
A continuación se muestra el programa dumpmice que despliega en pantalla la
información leída de dicho dispositivo.
/* PROGRAMA: dumpmice.c
FUNCION:
Imprime los datos que entrega el dispositivo
/dev/input/mice
USO:
./dumpmice

*/
#include <stdio.h>
#include <sys/types.h>
#include <sys/stat.h>
#include <fcntl.h>
#include <unistd.h>
#include <termios.h>

struct MiceStruct
{
unsigned char mode;
char x;
char y;
};

int main()
{
unsigned char ii = 0x10;
unsigned char m = 0x00;
char x,y;
struct MiceStruct data; // Struct to store data.
int fd = 0;
struct termios param;

// Opens device.
if ((fd = open("/dev/input/mice",O_RDONLY)) == -1)
{
perror("Error al abrir el dispositivo:
/dev/input/mice");
return -1;
}

/* Turns STDIN non-blocking (doesn't stops if there's no


input in scanf),
and sets STDIN to non-canonical (input doesn't wait
for ENTER). */
tcgetattr(0, &param);
param.c_lflag &= ~(ICANON|ECHO);
tcsetattr(0, TCSANOW, &param);
fcntl(0, F_SETFL, O_NONBLOCK);

printf("Press 'q' for Quit.\n\n");

while (1)
{
// Gets data from device.
if ( read(fd, &data, sizeof(struct MiceStruct)) < 0)
{
printf("Error leyendo datos\n");
perror("Read");
break;
}

m = data.mode;
//x = (int)data.x;
//y = (int)data.y;
x = data.x;
y = data.y;

if (ii & 0x10) // Prints header.


{
printf("|Overflow----X,Y|Sign bit----
X,Y|Always1|Mid Btn|Rig Btn|LeftBtn|X Mov--|Y Mov--|\n");
ii = 0;
}

// Prints formated data.

printf("|%d\t,%d\t|%d\t,%d\t|%d\t|%d\t|%d\t|%d\t|%d\
t|%d\t|\n",

(m&0x80)&&1,(m&0x40)&&1,(m&0x20)&&1,(m&0x10)&&1,(m&0
x08)&&1,(m&0x04)&&1,(m&0x02)&&1,(m&0x01)&&1,
x, y);

ii++;

if (getchar() == 'q') // Quits if 'q' key was


pressed.
{
printf ("exiting .\n");
break;
}
}

param.c_lflag |= ICANON|ECHO;
tcsetattr(0, TCSANOW, &param);
close (fd);
return 0;
}

Después, se muestra la compilación y la ejecución del programa:


[user@localhost home]$ gcc -o dumpmice dumpmice.c
[user@localhost home]$ sudo su
[user@localhost home]# ./dumpmice
Press 'q' for Quit.

|Overflow----X,Y|Sign bit----X,Y|Always1|Mid Btn|Rig


Btn|LeftBtn|X Mov--|Y Mov--|
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |0 ,0 |1 |0 |0 |1 |0 |0 |
|0 ,0 |0 ,0 |1 |0 |0 |0 |0 |0 |
|0 ,0 |0 ,0 |1 |0 |1 |0 |0 |0 |
|0 ,0 |0 ,0 |1 |0 |0 |0 |0 |0 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
|Overflow----X,Y|Sign bit----X,Y|Always1|Mid Btn|Rig
Btn|LeftBtn|X Mov--|Y Mov--|
|0 ,0 |0 ,0 |1 |0 |0 |1 |0 |0 |
|0 ,0 |0 ,0 |1 |0 |0 |0 |0 |0 |
|0 ,0 |1 ,0 |1 |0 |0 |0 |1 |-1 |
exiting .

Referencias:
Love, Robert. Linux System Programming, O'REILLY, 2007. Biblioteca:
QA76.76.O63 L69 2007

Marquez , Fernando, Programación Avanzada en UNIX, 3a. edicion, 2004. Biblioteca


QA, 76.76, .O63, M37, 2006 (Reserva)

Kurt Wall. Programación en Linux con Ejemplos. 2000. Prentice Hall. Biblioteca:
QA76.76.O63 W35718 2000 (Reserva)
Laboratorio:
Formulario del laboratorio

Laboratorio de
Sistemas Operativos

Práctica 9: Networking: Cliente-Servidor.


Autor: Dr. Juan A. Nolazco Flores Co-autores: M.C. Jorge Villaseñor, M.C. Roberto Aceves,
Ing.Raúl Fuentes, Dr. jose i. icaza, claudia.

ÍNDICE
Objetivo
Conceptos
Comunicación entre procesos
Alcance de una red de área personal
Sockets
Arquitectura/modelo Cliente-Servidor.
Cliente-Servidor Unix.
Ejemplo de socket local (de la familia AF_UNIX)
Ejemplo de socket remoto (de la familia AF_INET)
Anexo A – Llamadas a sistema utilizadas en la arquitectura cliente-servidor
Laboratorio

Objetivos
Siguiendo con los elementos de programación en un ambiente operativo GNU/Linux,
trabajarás en esta ocasión con los mecanismos utilizados por aplicaciones del tipo
“Cliente/Servidor”, utilizando protocolos de comunicación denominados “AF_UNIX”,
y “TCP/IP” (AF_INET)

Conceptos
Comunicación entre procesos
Hoy en día es fundamental que un proceso pueda comunicarse con otros (incluyendo
procesos entre aplicaciones de usuarios y entre procesos del propio S.O.) A este tipo
de comunicación se le denomina IPC por su escritura en inglés (“Inter-Process
Communication”).
Puede llevarse a cabo de diferentes formas, por ejemplo compartiendo un espacio
específico de memoria RAM (donde se almacenan variables o buffers compartidos) y
los accesos a esas áreas compartidas se realizan mediante las rutinas de IPC. Estas
rutinas proveen los mecanismos necesarios para que la comunicación sea siempre
sincronizada y exitosa (sin riesgo de una “Race condition” o problemas similares).
Este tipo de comunicación se requería desde antes de que una computadora
ocupará comunicarse con otra computadora y por lo mismo existe una familia de
protocolos en los sistemas UNIX que son heredados en los sistemas operativos
GNU/Linux. A esta familia se le denomina “AF_UNIX” y se utiliza para comunicar
procesos que residen en una misma máquina.

En la modernidad el conjunto de protocolos para Internet denominado “TCP/IP”es


probablemente la forma más popular para comunicarse con procesos residentes en
varias computadoras, aunque también puede utilizarse para establecer comunicación
entre procesos residentes en un mismo equipo.

Cuando un proceso Servidor central es accedido por varios procesos Cliente, la


comunicación se denomina “cliente-servidor”. El proceso cliente puede comunicarse
con el proceso servidor utilizando la IP (Internet protocol address) de la máquina
donde reside el proceso servidor, o bien su dirección lógica, típicamente (127.0.0.1).
En este laboratorio nos concetraremos en este tipo de comunicación entre pocesos
cliente y un proceso servidor.

Hoy en día los S.O.contemporáneos GNU/Linux ya reconocen versiones de IP


denominadas versión 4 IP(v4) y versión 6 (IPv6), por lo tanto manejan dos
familias de protocolos por separado denominadas: AF_INET y AF_INET6 Un
solo proceso servidor para utilizar ambas familias simultáneamente con
diferentes clientes.

Breve repaso sobre tipos de redes

Una red de área personal (Personal Area Network or PAN) es una red de
computadoras utilizada para la comunicación entre los dispositivos
electrónicos (teléfonos inteligentes, tabletas, computadoras) cercanos a una
persona. Los dispositivos pueden no necesariamente pertenecer a la persona
en cuestión.

El alcance de un PAN es típicamente algunos metros; rara vez se exceden 5


metros. Los PAN se pueden utilizar para la comunicación entre los mismos
dispositivos personales (comunicación intrapersonal), o para que uno de los
dispositivos de la red pueda conectarse a una red de más alto nivel y al Internet (un
enlace ascendente), dotando de este acceso a otros dispositivos.Las redes de área
personal puede estar conectadas con tecnologías tales como USB y FireWire.

Una red inalámbrica de área personal (WPAN) se puede también hacer posible con
tecnologías de red tales como IrDA, Bluetooth, UWB, Z-Wave y ZigBee. La
diferencia clave entre cada una de ellas es el consumo que exigen.

La siguiente figura indica algunos de los protocolos utilizados por Personal, Local,
Metropolitan and Wide area networks:
Sockets
En los orígenes de Internet, surgió la necesidad de intercomunicar procesos entre sí,
por lo que la Universidad de Berkeley propuso la primera especificación e
implementación de los sockets.

El socket es una interfaz que se compone de una serie de llamadas a sistema y que
permite que dos procesos (posiblemente situados en computadoras
distintas) intercambien un flujo de datos de forma fiable y ordenada. El elemento clave
del concepto esta en que los programas puedan comunicarse entre sí. Es necesario
que un programa sea capaz de localizar al otro y que ambos puedan intercambiar
cualquier secuencia de datos.

Cada socket requiere la definición de tres recursos:


 Un protocolo de comunicación que permita el intercambio de información
 Una dirección que conozca el protocolo de comunicación para identificar a los
dispositivos comunicantes
 Un número de puerto que identifique a un proceso dentro de los dispositivos.

A continuación se muestran los encabezados y la llamada a sistema socket(),


necesarios para la creación de un socket en C:

#include <sys/types.h>
#include <sys/socket.h>
int socket(int af, int type, int protocol);

Esta llamada a sistema regresa un file descriptor (fd), mediante el cual se puede
acceder a dicho socket. Recordemos que los fd’s pueden utilizarse no únicamente
para escribir y leer archivos, sino también para enviar y recibir datos entre procesos
 El parámetro af (address family) especifica la familia de protocolos que se
desea utilizar. Dos de estas familias, presentes en la mayoría de los sistemas,
son:

 AF_UNIX – protocolos internos Unix, para comunicar


procesos que se ejecutan en la misma máquina,

 AF_INET – protocolos Internet, para comunicar


procesos en diferentes máquinas mediante protocolos
de red, como TCP,otros: AF_ISO, AF_IPX, AF_APPLETALK,
etc…

 El parámetro type indica la semántica de la comunicación; algunos de los


valores que puede tomar son:

 SOCK_STREAM – orientado a conexión, secuencial,


confiable, 2 vías. Recordemos que con un protocolo
“orientado a conexión” los datos viajan por la red en
el mismo orden en que son enviados
 SOCK_DGRAM – no orientado a conexión (datagrama)- los
datos viajan en paquetes numerados que pueden viajar
por distintas rutas antes de llegar al receptor,
quien se encarga de ponerlos en orden
 SOCK_SEQPACKET – no orientado a conexión, secuencial,
confiable, para Xerox Network System

 El parámetro “protocol” indica el protocolo en particular que se va a utilizar; si


se deja con valor de 0 el sistema se encarga de elegirlo.

Arquitectura/modelo Cliente-Servidor.
Esta arquitectura consiste en un proceso (Cliente) que realiza peticiones a otro
proceso (Servidor), y este último le responde. Esta idea se puede aplicar a tanto a
procesos que se ejecutan en la misma computadora como a aquellos que se
encuentran corriendo en diferentes computadoras conectadas por una red.

 Un servidor es un proceso que se está ejecutando en alguna computadora


conectada a la red y que gestiona el acceso a un determinado recurso.

 Un cliente es un proceso que se ejecuta en la misma o diferente máquina y


que realiza peticiones de algún recurso que gestiona el servidor.

El servidor está continuamente esperando peticiones de servicio. Cuando se produce


una petición, el servidor despierta y atiende al cliente. Cuando el servicio concluye, el
servidor vuelve al estado de espera.

De acuerdo con la forma de prestar el servicio, podemos considerar dos tipos de


servidores:
 Interactivos, que atienden una sola petición de servicio a la vez; y
 Concurrentes, que toman cada petición y crean otros procesos para que se
encarguen de atenderla, permitiendo atender varias peticiones a la vez.
El siguiente diagrama de flujo (Fig. 1) muestra la estructura de los procesos servidores
y clientes.

Fig. 1. Estructura de los programas servidores y clientes.

NOTA: El servidor y el cliente deben “entenderse”; esto significa que si el


servidor espera que la comunicación se enviada de un cierto modo, el
proceso cliente debe de hacerlo de esa forma. Esto es ajeno al lenguaje de
programación con el cual se desarrollan los procesos. Tomen como ejemplo
a los navegadores más populares: Chrome, Safari, Internet Explorer, Firefox.
Cada uno fue hecho de forma diferente, pero todos saben que transacciones
realizar con los servidores web para recibir los archivos y códigos html para
desplegar ante el usuario la página web solicitada.

Los servicios que intercambian el cliente y el servidor varían según el lenguaje de


programación y el nivel dentro del Modelo OSI que estemos utilizando. Para esta
práctica estaremos utilizando la familia más sencilla que deja los datos en nivel
“Presentación” de OSI (nivel Aplicación de TCP/IP) y se le recomienda al alumno
utilizar variables tipo “char” para no tener problemas de conversión de valores.

En este nivel prácticamente solo contamos con funciones de enviar (usualmente


denominadas “write”) y funciones de recibir (usualmente denominadas “read”) que
pueden recibir cualquier tipo de dato, y queda en responsabilidad del diseñador
realizar la conversión o casting de dicha información.

Cliente-Servidor Unix.
Para implementar un esquema de comunicación cliente-servidor entre procesos, se
utilizan diversas llamadas al sistema, tales como write() o read(). Si te
resultan familiares es debido a que en en Unix “everything is a file”, asi que la
comunicación entre dos computadoras distintas también se lleva a cabo por medio de
un “File” que en realidad es un canal de comunicación entre las dos procesos. En el
siguiente diagrama (Fig. 2) puedes observar la secuencia de llamadas a sistema
usadas para una comunicación orientada a conexión:

Fig. 2. Secuencia de llamadas para una comunicación orientada a conexión

La Figura 3 muestra la secuencia de llamadas para una comunicación no orientada a


conexión.
Fig. 3. Secuencia de llamadas para una comunicación no orientada a conexión.

El Anexo A contiene la definición de las llamadas a sistema utilizadas en el proceso


de conexión por sockets, que son parte de la API definida por la biblioteca de sockets
de Berkeley.

 Ejemplo de socket local (de la familia AF_UNIX)


Este ejemplo muestra la comunicación entre dos programas que corren en la misma
máquina, por lo que no necesitan una conexión de red. Trabaja con un protocolo
orientado a conexión (flujo de datos).

 El Código 1 muestra el cliente


 El Código 2 el servidor.

Observa las llamadas a sistema que se utilizan, en particular la creación del socket
donde se especifica la familia de direcciones de protocolos del socket y su semántica:
socket(AF_UNIX, SOCK_STREAM, 0);

/*
* client1.c - a simple local client program.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>

int main()
{
int sockfd;
int len;
struct sockaddr_un address;
int result;
char ch = 'A';
/* Create a socket for the client. */
sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

/* Name the socket, as agreed with the server. */


address.sun_family = AF_UNIX;
strcpy(address.sun_path, "server_socket");
len = sizeof(address);

/* Now connect our socket to the server's socket. */


result = connect(sockfd, (struct sockaddr *)&address, len);

if(result == -1) {
perror("oops: client1");
exit(1);
}

/* We can now read/write via sockfd. */


write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
close(sockfd);
return 0;
}

Código 1. Cliente local.

/*
* server1.c - a simple local server program.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <sys/un.h>
#include <unistd.h>

int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_un server_address;
struct sockaddr_un client_address;

/* Remove any old socket and create an unnamed socket for the server. */
unlink("server_socket");
server_sockfd = socket(AF_UNIX, SOCK_STREAM, 0);

/* Name the socket. */


server_address.sun_family = AF_UNIX;
strcpy(server_address.sun_path, "server_socket");
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

/* Create a connection queue and wait for clients. */


listen(server_sockfd, 5);
while(1) {
char ch;

printf("server waiting\n");

/* Accept a connection. */
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);
/* We can now read/write to client on client_sockfd. */
read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}

Código 2. Servidor local)

 Ejemplo de Socket Remoto (Familia AF_INET)


Este ejemplo muestra la comunicación entre dos programas que corren en distintas
máquinas, ambas conectadas a una red. Trabaja con un protocolo orientado a
conexión (flujo de datos).

 El Código 3 muestra el cliente


 El Código 4 el servidor.

Observa la creación del socket con la familia AF_INET y el uso de la estructura


sockaddr_in para identificar al socket donde se definen la familia de direcciones, la
dirección de red y el puerto.

/*
* client2.c - a simple network client program.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
int sockfd;
int len;
struct sockaddr_in address;
int result;
char ch = 'A';

/* Create a socket for the client. */


sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* Name the socket, as agreed with the server. */


address.sin_family = AF_INET;
address.sin_addr.s_addr = inet_addr("127.0.0.1");
address.sin_port = 9734;
len = sizeof(address);

/* Now connect our socket to the server's socket. */


result = connect(sockfd, (struct sockaddr *)&address, len);

if(result == -1) {
perror("oops: client2");
exit(1);
}
/* We can now read/write via sockfd. */
write(sockfd, &ch, 1);
read(sockfd, &ch, 1);
printf("char from server = %c\n", ch);
close(sockfd);
return 0;
}

Código 3. Cliente remoto.

/*
* server2.c - a simple network server program.
*/
#include <sys/types.h>
#include <sys/socket.h>
#include <stdio.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <unistd.h>

int main()
{
int server_sockfd, client_sockfd;
int server_len, client_len;
struct sockaddr_in server_address;
struct sockaddr_in client_address;

/* Create an unnamed socket for the server. */


server_sockfd = socket(AF_INET, SOCK_STREAM, 0);

/* Name the socket. */


server_address.sin_family = AF_INET;
server_address.sin_addr.s_addr = inet_addr("127.0.0.1");
server_address.sin_port = 9734;
server_len = sizeof(server_address);
bind(server_sockfd, (struct sockaddr *)&server_address, server_len);

/* Create a connection queue and wait for clients. */


listen(server_sockfd, 5);
while(1) {
char ch;

printf("server waiting\n");

/* Accept a connection. */
client_len = sizeof(client_address);
client_sockfd = accept(server_sockfd,
(struct sockaddr *)&client_address, &client_len);

/* We can now read/write to client on client_sockfd. */


read(client_sockfd, &ch, 1);
ch++;
write(client_sockfd, &ch, 1);
close(client_sockfd);
}
}

Código 4. Servidor remoto.


Para compilar cualquiera de estos códigos puedes utilizar la sintaxis que hemos
manejado en otras prácticas:
gcc –o nombre_ejecutable nombre_codigo.c

Anexo A
Llamadas a sistema utilizadas en la arquitectura cliente-
servidor
A continuación, una lista con las llamadas a sistema utilizadas en el proceso de
conexión por sockets, que son parte de la API definida por la librería de sockets de
Berkeley:

 socket() - creates a new socket of a certain socket type, identified by an


integer number, and allocates system resources to it. Creates an
endpoint for communication and returns a file descriptor for the socket.
socket() takes three arguments:

 domain, which specifies the protocol family of the created socket. For
example: PF_INET for network protocol IPv4 or PF_INET6 for IPv6.
PF_UNIX for local socket (using a file).

 type, one of:


 SOCK_STREAM (reliable stream-oriented service or Stream
Sockets),
 SOCK_DGRAM (datagram service or Datagram Sockets),
 SOCK_SEQPACKET (reliable sequenced packet service)
 SOCK_RAW (raw protocols atop the network layer).

 protocol, specifying the actual transport protocol to use. The most


common are IPPROTO_TCP, IPPROTO_SCTP, IPPROTO_UDP,
IPPROTO_DCCP. These protocols are specified in <netinet/in.h>.
The value “0” may be used to select a default protocol from the
selected domain and type.

The function returns -1 if an error occurred. Otherwise, it returns an integer


representing the newly-assigned descriptor. Prototype:
#include <sys/types.h>
#include <sys/socket.h>
int socket(int domain, int type, int protocol);

 bind() - is typically used on the server side, and associates a socket with a
socket address structure, i.e. a specified local port number and IP address.
Assigns a socket an address. When a socket is created using socket(), it is only
given a protocol family, but not assigned an address. This association with an
address must be performed with the bind() system call before the socket can
accept connections to other hosts. bind() takes three arguments:

 sockfd, a descriptor representing the socket to perform the bind on


 my_addr, a pointer to a sockaddr structure representing the address to
bind to.

 addrlen, a socklen_t field specifying the size of the sockaddr structure.

Bind() returns 0 on success and -1 if an error occurs. Prototype:


#include <sys/types.h>
#include <sys/socket.h>
int bind(int sockfd, const struct sockaddr *my_addr,
socklen_t addrlen);

 listen() - is used on the server side, and causes a bound TCP socket to enter
listening state. After a socket has been associated with an address, listen()
prepares it for incoming connections. However, this is only necessary for the
stream-oriented (connection-oriented) data modes, i.e., for socket types
(SOCK_STREAM, SOCK_SEQPACKET). listen() requires two arguments:

 sockfd, a valid socket descriptor.

 backlog, an integer representing the number of pending connections


that can be queued up at any one time. The operating system usually
places a cap on this value.

Once a connection is accepted, it is dequeued. On success, 0 is returned. If an


error occurs, -1 is returned.
Prototype
#include <sys/socket.h>
int listen(int sockfd, int backlog);

 accept() - is used on the server side. It accepts a received incoming attempt to


create a new TCP connection from the remote client, and creates a new socket
associated with the socket address pair of this connection. When an application
is listening for stream-oriented connections from other hosts, it is notified of
such events (cf. select() function) and must initialize the connection using the
accept() function. Accept() creates a new socket for each connection and
removes the connection from the listen queue. It takes the following arguments:

 sockfd, the descriptor of the listening socket that has the connection
queued.

 cliaddr, a pointer to a sockaddr structure to receive the client's address


information.

 addrlen, a pointer to a socklen_t location that specifies the size of the


client

 address structure passed to accept(). When accept() returns, this


location indicates how many bytes of the structure were actually used.

The accept() function returns the new socket descriptor for the accepted
connection, or -1 if an error occurs. All further communication with the remote
host now occurs via this new socket. Datagram sockets do not require
processing by accept() since the receiver may immediately respond to the
request using the listening socket. Prototype:
#include <sys/types.h>
#include <sys/socket.h>
int accept(int sockfd, struct sockaddr *cliaddr,
socklen_t *addrlen);

 connect() - is used on the client side, and assigns a free local port number to
a socket. In case of a TCP socket, it causes an attempt to establish a new TCP
connection. The connect() system call connects a socket, identified by its file
descriptor, to a remote host specified by that host's address in the argument list.
Certain types of sockets are connectionless, most commonly user datagram
protocol sockets. For these sockets, connect takes on a special meaning: the
default target for sending and receiving data gets set to the given address,
allowing the use of functions such as send() and recv() on connectionless
sockets.
connect() returns an integer representing the error code: 0 represents success,
while -1 represents an error. Prototype:
#include <sys/types.h>
#include <sys/socket.h>
int connect(int sockfd, const struct sockaddr
*serv_addr, socklen_t addrlen);

 gethostbyname() and gethostbyaddr() - The gethostbyname() and


gethostbyaddr() functions are used to resolve host names and addresses in the
domain name system or the local hosts other resolver mechanisms (e.g.,
/etc/hosts lookup). They return a pointer to an object of type struct hostent,
which describes an Internet Protocol host. The functions take the following
arguments:

 name specifies the name of the host. For example:


www.wikipedia.org

 addr specifies a pointer to a struct in_addr containing the address of


the host.

 len specifies the length, in bytes, of addr.

 type specifies the address family type (e.g., AF_INET) of the host
address.

The functions return a NULL pointer in case of error, in which case the external
integer h_errno may be checked so see whether this is a temporary failure or an
invalid or unknown host. Otherwise a valid struct hostent * is returned.These
functions are not strictly a component of the BSD socket API, but are often used
in conjunction with the API functions. Furthermore, these functions are now
considered legacy interfaces for querying the domain name system. New
functions that are completely protocol-agnostic have been defined.

These new function are getaddrinfo() and getnameinfo(), and are based on a
new addrinfo data structure. Prototypes:
struct hostent *gethostbyname(const char *name);
struct hostent *gethostbyaddr(const void *addr, int
len, int type);

 send() and recv(), or write() and read(), or recvfrom() and sendto(), -


are used for sending and receiving data to/from a remote socket.

 close() - causes the system to release resources allocated to a socket. In


case of TCP, the connection is terminated.

==== Laboratorio ====


ssize_t read(int fd, char*Char, size_t size);
ssize_t write(int fd, char* Char, size_t size);

char* char - Recibe una cadena de caracteres; para que no batallen, utilicen un
arreglo del tipo Char. La longitud del arreglo debe estar indicado también en "size_t
size" (que es el tamaño del buffer de datos que se va a enviar). No olviden que
tendrán que utilizar ampersand ( & ) en la función de read ya que el buffer leído se
depositará en "char* char)
ejemplo. read(client_sockfd, &Char, sizeof(Char))

 Primera parte: Cliente-Servidor en Procesos (Familia UNIX)


Ejercicio 1
Modifique el programa del cliente para que envíe cualquier carácter (un solo Char) al
servidor. Dicho carácter debe ser dado por el usuario al invocar el programa (Como
argumento de entrada)

Nota: Reportarás en el formulario el código fuente del cliente ya modificado.

Ejercicio 2
Compile los códigos de cliente y servidor local, ejecute el servidor en el fondo y ejecute
un par de clientes en la misma terminal.

Tip: Si utiliza un comando como:


$ Servidor &
Cliente [Argumento] && Cliente [Argumento]...
estarás enviando el servidor a ejecutar en el background, liberando la
terminal para aceptar comandos adicionales, seguido de invocaciones a los
clientes.
O tambien se utiliza como
$ Servidor & Cliente [Argumento] ; Cliente [Argumento] ; ...

Nota: Reportarás en el formulario el despliegue de la terminal del arranque del servidor


y la ejecución de los clientes (Con las respectivas respuestas del servidor).

Ejercicio 3
Confirme el funcionamiento del servidor mediante Netstat
Netstat es una herramienta incluida en sistemas GNU/Linux la cual permite revisar
qué recursos están separados para la comunicación entre procesos de AF_UNIX y
para la comunicación con TCP/IP (AF_inet). Si se ejecuta el comando sin argumentos
se recibirá una gran cantidad de información, por lo tanto tenemos que elaborar algo
de filtrado.

Como el objetivo es validar que una aplicación servidor para la familia AF_UNIX está
registrado y en estado de escucha (Listening), se espera que verifiques (man….) qué
argumentos te permiten desplegar los servicios con esas características.
Seguramente saldrán múltiples servicios, la mayoría internos del S.O. por lo que debes
asegurar que la salida esté filtrada (grep?) y solo muestre el proceso que nos
interesa. Para facilitar esto último puedes extraer el PROCESS-ID del servidor de
antemano (comando $ ps con ciertos argumentos…) y utilizarlo en este punto.

NOTA: El alumno reportará en el formulario el comando introducido para validar la


existencia del servidor y lo desplegado en la terminal.

 Segunda parte: Cliente-Servidor (Familia INET)


Se cambiarán ambos programas para que el Servidor funcione como un Log. En
general el servidor desplegará el siguiente mensaje cada vez que un cliente se
comunique:

<Dirección IP del cliente>: <Mensaje entregado por el cliente >

El mensaje tendrá una longitud máxima de 50 caracteres.


Una corrida ejemplo del servidor sería como la siguiente:

$ ./Servidor
10.17.112.90: Mensaje 1
10.17.112.70: Mensaje 2
10.17.112.80: Mensaje 3

La corrida ejemplo del cliente 10.17.112.90 sería:

$ ./Cliente Mensaje1
Servidor: Recibido

NOTA: El alumno puede decidir cómo pedir el mensaje a enviar y que


desplegar en pantalla, pero siempre deberá desplegar la respuesta “Recibido”
del servidor.

Entregas:
 Entregar el código fuente del servidor ya modificado.
 Entregar el código fuente del cliente ya modificado.
 Entregar un ejemplo de corrida del servidor y dos o más clientes.

Tip: El alumno puede utilizar la dirección lógica 127.0.0.1 para conectar al


servidor de tal forma puede evitar la necesidad de usar otros equipos de computo.
O puede conectarse por ssh a la maquina remota [email protected]
editar mediante nano o vim, compilar con gcc o g++ y correr el programa
en la computadora remota. La direccion Ip de la maquina remota es
201.156.75.248

 Laboratorio: Modelo Ciente-Servidor.


not connection-oriented...

COMPLEMENTO 3

LABORATORIO DE SISTEMAS OPERATIVOS

Práctica 9.1: Cliente-servidor con Wiimote

I.Antecedentes
II.Bluetooth.
i.Conceptos esenciales de la programación Bluetooth.
ii.Cliente-Servidor Bluetooth.
III. Wiimote.
IV.Cliente-Servidor Wiimote.
V.Librería CWiid
VI.Anexo A
VII.Anexo B.
VIII.Referencias
IX.FAQ:
X.Laboratorio
i.Librería CWiid.
i.Prerequisitos
ii.Comprobar el correcto funcionamiento de la librería cwiid.
ii. Cliente-Servidor Wiimote.
iii.Conectarse a una PC por Bluetooth y hacer "dump" de un "ping" (l2ping)
XI. Reporte segun lo que se le pida en el formulario

Antecedentes
Esta es la continuación de la practica de Cliente-Servidor.Se puede acceder a ella desde
esta liga.

Bluetooth.

“Bluetooth es una especificación industrial para Redes Inalámbricas de Área


Personal (WPANs) que posibilita la transmisión de voz y datos entre diferentes
dispositivos mediante un enlace por radiofrecuencia segura y globalmente libre
(2,4 GHz.). Los principales objetivos que se pretende conseguir con esta norma
son:
∙ Facilitar las comunicaciones entre equipos móviles y fijos.
∙ Eliminar cables y conectores entre éstos.
∙ Ofrecer la posibilidad de crear pequeñas redes inalámbricas y facilitar
la sincronización de datos entre nuestros equipos personales.

Bluetooth se denomina al protocolo de comunicaciones diseñado especialmente


para dispositivos de bajo consumo, con una cobertura baja y basados en
transceptores de bajo coste.

La clasificación de los dispositivos Bluetooth como "Clase 1", "Clase 2" o "Clase 3"
es únicamente una referencia de la potencia de trasmisión del dispositivo, siendo
totalmente compatibles los dispositivos de una clase con los de la otra.

Potencia máxima permitida Potencia máxima permitida Rango


Clase
(mW) (dBm) (aproximado)
Clase 100 mW 20 dBm ~100 metros
1
Clase
2.5 mW 4 dBm ~20 metros
2
Clase
1 mW 0 dBm ~1 metro
3

En cuanto al ancho de banda:

Versión Ancho de banda


Versión 1.2 1 Mbit/s
Versión 2.0 + EDR 3 Mbit/s
UWB Bluetooth
53 - 480 Mbit/s
(propuesto)
“ [4] .

Conceptos esenciales de la programación Bluetooth.

Los dispositivos Bluetooth, al igual que los dispositivos de red, tienen un


identificador numérico (dirección) único de 48-bit, por ejemplo:
0x000EED3D1829. Estos dispositivos también tienen un nombre amigable al
usuario, similar a los nombres de dominio (como www.google.com), llamado
display name, un ejemplo sería “Mi Telefono”.

En lo que se refiere a privacidad y consumo de energía, los dispositivos Bluetooth


tienen dos opciones que determinan si el dispositivo responde a preguntas de
otros dispositivos o no, y si responde o no a intentos de conexión. La opción
Inquiry Scan indica si un dispositivo es detectable por otros dispositivos Bluetooth.
Page Scan indica si el dispositivo acepta conexiones de otros dispositivos
Bluetooth que conocen su dirección. Generalmente la configuración por defecto de
los dispositivos es: no detectable y acepta conexiones.

Existen algunos protocolos de transporte usados en la comunicación a través de


dispositivos Bluetooth, que se distinguen por las garantías y la semántica de su
operación. Algunos de ellos son RFCOMM, L2CAP, ACL y SCO, listados de alto a bajo
nivel. El más relevante para esta práctica es RFCOMM (Radio Frequency
Communications), que es un protocolo de transporte de propósito general
orientado a emular puertos seriales. Este protocolo tiene 30 puertos, en
comparación con TCP que tienen hasta 65,535.

Cliente-Servidor Bluetooth.

Es posible conectar dos dispositivos, en el esquema cliente-servidor, a través de


adaptadores Bluetooth y aprovechar la ventaja de la movilidad que esta tecnología
permite. El intercambio de información entre los dispositivos se lleva a cabo
utilizando sockets, configurados para trabajar con algunos de los protocolos de
comunicación Bluetooth.

Como ejemplo se muestran los programas rfcomm-client.c (cód. 5), que es un


cliente que se conecta mediante Bluetooth al programa servidor rfcomm-server.c
(cód. 6). Observa que la familia de direcciones con la que se crea el socket es
AF_BLUETOOTH, el tipo de socket es orientado a conexión (SOCK_STREAM), y el
protocolo de transporte es RFCOMM para Bluetooth (BTPROTO_RFCOMM). Luego,
para realizar la conexión se usará la familia de direcciones como AF_BLUETOOTH,
con el puerto número 1, y la dirección del servidor "01:23:45:67:89:AB". El programa
rfcomm-server2.c (cód. 7) muestra un servidor que acepta múltiples conexiones.

Para compilar el siguiente código, se debe ligar con la biblioteca compartida de


bluetooth con la bandera
'-lbluetooth' qeudando de esta manera:
gcc -o Ejecutable rfcomm-client.c -lbluetooth

NOTA: Si tiene problemas compilando revise primero los pre-requisitos del laboratorio.

/*
* rfcomm-client.c – sends a message to a BT device.
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

int main(int argc, char **argv)


{
struct sockaddr_rc addr = { 0 };
int s, status;
char dest[18] = "01:23:45:67:89:AB"; // Server BT device address

// allocate a socket
s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

// set the connection parameters (who to connect to)


addr.rc_family = AF_BLUETOOTH;
addr.rc_channel = 1;
str2ba( dest, &addr.rc_bdaddr );

// connect to server
status = connect(s, (struct sockaddr *)&addr, sizeof(addr));

// send a message
if( 0 == status ) {
status = send(s, "hello!", 6, 0);
}

if( status < 0 ) perror("uh oh");

close(s);
return 0;
}

Código 5. Cliente bluetooth.


/*
* rfcomm-server.c – receives a message from a BT device.
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

int main(int argc, char **argv)


{
struct sockaddr_rc loc_addr = { 0 }, rem_addr = { 0 };
char buf[1024] = { 0 };
int s, client, bytes_read;
unsigned int opt = sizeof(rem_addr);

// allocate socket
s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

// bind socket to port 1 of the first available bluetooth adapter


loc_addr.rc_family = AF_BLUETOOTH;
loc_addr.rc_bdaddr = *BDADDR_ANY;
loc_addr.rc_channel = 1;
bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));

// put socket into listening mode


listen(s, 1);

// accept one connection


client = accept(s, (struct sockaddr *)&rem_addr, &opt);

ba2str( &rem_addr.rc_bdaddr, buf );


fprintf(stderr, "accepted connection from %s\n", buf);
memset(buf, 0, sizeof(buf));

// read data from the client


bytes_read = recv(client, buf, sizeof(buf), 0);
if( bytes_read > 0 ) {
printf("received [%s]\n", buf);
}

// close connection
close(client);
close(s);
return 0;
}

Código 6. Servidor bluetooth.

/*
* rfcomm-server2.c – receives a message from a BT device.
*/
#include <stdio.h>
#include <unistd.h>
#include <sys/socket.h>
#include <bluetooth/bluetooth.h>
#include <bluetooth/rfcomm.h>

int main(int argc, char **argv)


{
struct sockaddr_rc loc_addr = { 0 }, rem_addr = { 0 };
char buf[1024] = { 0 };
int s, client, bytes_read;
unsigned int opt = sizeof(rem_addr);

// allocate socket
s = socket(AF_BLUETOOTH, SOCK_STREAM, BTPROTO_RFCOMM);

// bind socket to port 1 of the first available bluetooth adapter


loc_addr.rc_family = AF_BLUETOOTH;
loc_addr.rc_bdaddr = *BDADDR_ANY;
loc_addr.rc_channel = 1;
bind(s, (struct sockaddr *)&loc_addr, sizeof(loc_addr));

// put socket into listening mode


listen(s, 5);

while (1)
{
// accept one connection
client = accept(s, (struct sockaddr *)&rem_addr, &opt);

ba2str( &rem_addr.rc_bdaddr, buf );


printf("accepted connection from %s\n", buf);
memset(buf, 0, sizeof(buf));

// read data from the client


bytes_read = recv(client, buf, sizeof(buf), 0);
if( bytes_read > 0 ) {
printf("received [%s]\n", buf);
}

// close connection
close(client);
}
close(s);
return 0;
}

Código 7. Servidor bluetooth para multiples conexiones.

Wiimote.
“The Wii Remote, sometimes unofficially nicknamed "Wiimote", is the primary

controller for Nintendo's Wii consol


e. A main feature of the Wii Remote is its motion sensing capability, which allows
the user to interact
with and manipulate items on screen via movement and pointing through the use
of accelerometer and optical sensor technology. Another feature is its
expandability through the use of attachments” [3].

“It contains a 1024x768 infrared camera with built-in hardware blob tracking of
up to 4 points at 100Hz. This significantly out performs any PC "webcam" available
today. It also contains a +/-3g 8-bit 3-axis accelerometer also operating at 100Hz
and an expansion port for even more capability” [1].

Cliente-Servidor Wiimote.

Es posible conectar dos dispositivos, de forma inalámbrica, usando la tecnología


Bluetooth en un esquema cliente-servidor. En esta sección se mostrará como hacer
algo similar con un control de la consola de juegos Wii (Wiimote).

El esquema de trabajo que se propone es: una computadora actúa como servidor
que pide reiteradamente (polling) información al cliente, que en este caso será el
control Wiimote. La información que entrega el cliente corresponde al estado de
los botones del control Wiimote.

La comunicación, en este esquema de trabajo en particular, entre una aplicación


(servidor) y el control Wiimote se realiza a través de varias capas o niveles. La
Figura 1 muestra estas capas. La primera, Aplicación, representa a la aplicación que
desarrollamos y deseamos que tenga capacidad de intercambiar datos a través de
una interfaz Bluetooth; la segunda, HID (Human Interface Device), equivale a un
driver que sirve para conectarse con un dispositivo de interfaz con humanos, en
este caso el control Wiimote; la tercera, HCI (Host Controller Interface), provee una
interfaz para comunicarse con el hardware Bluetooth; USB permite la
comunicación con el adaptador Bluetooth a través del puerto USB; el adaptador
Bluetooth se encarga del manejo de la comunicación vía radio.
Aplicación
HID
HCI
USB
Adaptador Bluetooth
Figura 1. Bluetooth stack.
Figura 1

HCI (Host Controller Interface) provee una interfaz uniforme para accesar a las
capacidades del hardware Bluetooth. Usando HCI una aplicación puede accesar el
hardware de Bluetooth sin necesidad de conocer detalles sobre la implementación
del hardware o de la capa de transporte. HCI provee algunas herramientas para
interactuar con un adaptador Bluetooth, entre ellas se encuentran hciconfig, que
sirve para configurar dispositivos Bluetooth y obtener información del mismo, y
hcitool, que se usa para configurar conexiones Bluetooth, entre otras.

Los dispositivos Bluetooth se identifican con el nombre de interfase hci seguido de


un número, empezando por el cero. Por ejemplo, en una computadora que cuente
con solo un adaptador Bluetooth éste se identificará como hci0. Puedes revisar si el
adaptador se encuentra habilitado (UP) y ver su dirección con el comando
hciconfig, como se muestra a continuación:
$ hciconfig
hci0: Type: USB
BD Address: 00:1E:37:DD:9F:3E ACL MTU: 1017:8 SCO MTU: 64:8
UP RUNNING PSCAN ISCAN
RX bytes:1238 acl:0 sco:0 events:30 errors:0
TX bytes:616 acl:0 sco:0 commands:30 errors:0

La herramienta hcitool nos permite ver qué dispositivos bluetooth se encuentran


en estado descubrible, por ejemplo:
$ hcitool scan
--> 00:19:1D:BC:3F:07 Nintendo RVL-CNT-01

Otra herramienta, hcidump, nos permite vaciar los datos que un dispositivo con
interfase Bluetooth entrega al driver HCI. En el Anexo B se muestra una versión
reducida de la herramienta hcidump donde se muestra explícitamente la
comunicación con el dispositivo utilizando sockets, en un esquema no orientado a
conexión. En la Figura 2 puedes ver la secuencia de llamadas a sistema utilizada en
la comunicación con el control Wiimote.

Proceso hciwiidump Proceso


Wiimote
socket() Apertura socket()
del canal

Liga socket
bind() con bind()
dirección
Petición de
servicio
recepción de mensajes por
recvmsg() sendmsg()
sondeo(polling)

Fig. 2. Secuencia de llamadas a


sistema, para una comunicación no
orientada a conexión, usada en el
código hciwiidump.

Librería CWiid

La librería CWiid es una colección de herramientas de Linux, escritas en lenguaje C,


que sirven como interfaz con el control Wiimote de Nintendo. En la Figura 3 se
muestra una captura de pantalla de la interfaz gráfica que sirve para ver el estado
de los sensores del Wiimote.

Figura 3. CWiid, interfaz gráfica.

Para emplear el dispositivo Wiimote en el sistema operativo Linux se precisan los


siguientes elementos:
● Un adaptador Bluetooth operativo (puede ser un adaptador
portátil tipo USB).
● Un Wiimote.
● Una distribución de Linux: Ubuntu (Debian) o Fedora
correctamente instalada
● El compilador GNU de Lenguaje C: gcc, correctamente instalado y
configurado (estándar por defecto en la mayoría de las distribuciones para
desarrolladores)
● Una librería de acceso a la pila de protocolos Bluetooth (bluez)
● Una librería de acceso al dispositivo Wiimote (cwiid)

La librería cwiid contiene los siguientes archivos:

libcwiid1 - library to interface with the wiimote


libcwiid1-dev - library to interface with the wiimote -- development
lswm - wiimote discover utility
python-cwiid - library to interface with the wiimote
wmgui - GUI interface to the wiimote
wminput - Userspace driver for the wiimote

Probarás su funcionamiento en la sección de Laboratorio.


Figura 4 - Atributos enviados por los sensores del wiimote

Referencias principales en web para la instalación:

 http://www.wiili.org/index.php/CWiid
 http://www.cwiid.org (ó http://abstrakraft.org/cwiid)
 https://help.ubuntu.com/community/CWiiD
 http://www.circuitdb.com/articles/7/3

Anexo A
Esta liga es un tema de referencia, maneja el concepto cliente-servidor para dentro de
una computadora, por medio de TCP/IP y por medio de blueetooth.
https://docs.google.com/Doc?docid=0Ae4E85DTQTe0ZGhuZmZmc182MmRzem42N3
hm&hl=es

Anexo B.

/*
* hciwiidump.c
*
* BlueZ - Bluetooth protocol stack for Linux
*
* Copyright (C) 2000-2002 Maxim Krasnyansky <[email protected]>
* Copyright (C) 2003-2007 Marcel Holtmann <[email protected]>
*
*
* This program is free software; you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation; either version 2 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with this program; if not, write to the Free Software
* Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
*
*/
/* Modificado para hacer más claro el proceso de obtención de datos de un
* control Wiimote mediante el protocolo Bluetooth
*/
#ifdef HAVE_CONFIG_H
#include <config.h>
#endif

#include <stdio.h>
#include <errno.h>
#include <fcntl.h>
#include <unistd.h>
#include <stdlib.h>
#include <string.h>
#include <getopt.h>
#include <sys/poll.h>
#include <sys/stat.h>
#include <sys/types.h>
#include <sys/ioctl.h>
#include <sys/socket.h>

#include <bluetooth/bluetooth.h>
#include <bluetooth/hci.h>
#include <bluetooth/hci_lib.h>

#include "parser/parser.h"

#if __BYTE_ORDER == __LITTLE_ENDIAN


static inline uint64_t ntoh64(uint64_t n)
{
uint64_t h;
uint64_t tmp = ntohl(n & 0x00000000ffffffff);
h = ntohl(n >> 32);
h |= tmp << 32;
return h;
}
#elif __BYTE_ORDER == __BIG_ENDIAN
#define ntoh64(x) (x)
#else
#error "Unknown byte order"
#endif
#define hton64(x) ntoh64(x)

#define SNAP_LEN HCI_MAX_FRAME_SIZE


#define DEFAULT_PORT "10839";

/* Default options */
static int snap_len = SNAP_LEN;
static int permcheck = 1;

struct hcidump_hdr {
uint16_t len;
uint8_t in;
uint8_t pad;
uint32_t ts_sec;
uint32_t ts_usec;
} __attribute__ ((packed));
#define HCIDUMP_HDR_SIZE (sizeof(struct hcidump_hdr))

struct btsnoop_pkt {
uint32_t size; /* Original Length */
uint32_t len; /* Included Length */
uint32_t flags; /* Packet Flags */
uint32_t drops; /* Cumulative Drops */
uint64_t ts; /* Timestamp microseconds */
uint8_t data[0]; /* Packet Data */
} __attribute__ ((packed));
#define BTSNOOP_PKT_SIZE (sizeof(struct btsnoop_pkt))

// Procesa la informacion que optiene del dispositivo HCI


static int process_frames(int dev, int sock, int fd, unsigned long flags)
{
struct cmsghdr *cmsg;
struct msghdr msg;
struct iovec iv;
struct hcidump_hdr *dh;
struct btsnoop_pkt *dp;
struct frame frm;
struct pollfd fds[2];
int nfds = 0;
char *buf, *ctrl;
int len, hdr_size = HCIDUMP_HDR_SIZE;

if (sock < 0)
return -1;

if (snap_len < SNAP_LEN)


snap_len = SNAP_LEN;

if (flags & DUMP_BTSNOOP)


hdr_size = BTSNOOP_PKT_SIZE;

buf = malloc(snap_len + hdr_size);


if (!buf) {
perror("Can't allocate data buffer");
return -1;
}

dh = (void *) buf;
dp = (void *) buf;
frm.data = buf + hdr_size;

ctrl = malloc(100);
if (!ctrl) {
free(buf);
perror("Can't allocate control buffer");
return -1;
}

if (dev == HCI_DEV_NONE)
printf("system: ");
else
printf("device: hci%d ", dev);

printf("snap_len: %d filter: 0x%lx\n", snap_len, parser.filter);

memset(&msg, 0, sizeof(msg));

fds[nfds].fd = sock;
fds[nfds].events = POLLIN;
fds[nfds].revents = 0;
nfds++;

while (1) {
// poll() revisa si un conjunto de file descriptors para ver si alguno
// de ellos esta listo para una operacion de I/O
int i, n = poll(fds, nfds, -1);
if (n <= 0)
continue;

for (i = 0; i < nfds; i++) {


if (fds[i].revents & (POLLHUP | POLLERR | POLLNVAL)) {
if (fds[i].fd == sock)
printf("device: disconnected\n");
else
printf("client: disconnect\n");
return 0;
}
}

iv.iov_base = frm.data;
iv.iov_len = snap_len;

msg.msg_iov = &iv;
msg.msg_iovlen = 1;
msg.msg_control = ctrl;
msg.msg_controllen = 100;

// recibe los datos en un socket no orientado a conexion (man recvmsg)


len = recvmsg(sock, &msg, MSG_DONTWAIT);
if (len < 0) {
if (errno == EAGAIN || errno == EINTR)
continue;
perror("Receive failed");
return -1;
}

/* Process control message */


frm.data_len = len;
frm.dev_id = dev;
frm.in = 0;
frm.pppdump_fd = parser.pppdump_fd;
frm.audio_fd = parser.audio_fd;

// remueve los headers del fame (desempaqueta el frame)


cmsg = CMSG_FIRSTHDR(&msg);
while (cmsg) {
switch (cmsg->cmsg_type) {
case HCI_CMSG_DIR:
frm.in = *((int *) CMSG_DATA(cmsg));
break;
case HCI_CMSG_TSTAMP:
frm.ts = *((struct timeval *) CMSG_DATA(cmsg));
break;
}
cmsg = CMSG_NXTHDR(&msg, cmsg);
}

frm.ptr = frm.data;
frm.len = frm.data_len;

parser.state=0;
// imprime la informacion
if (parser.flags & DUMP_RAW)
raw_ndump(0, &frm,-1);
else
hci_dump(0, &frm);
fflush(stdout);
}

return 0;
}

// Crea un socket y regresa un file descriptor del mismo.


static int open_socket(int dev, unsigned long flags)
{
struct sockaddr_hci addr;
struct hci_filter flt;
struct hci_dev_info di;
int sk, dd, opt;

// Revisa que se pueda acceder al dispositivo HCI


if (permcheck && dev != HCI_DEV_NONE) {
dd = hci_open_dev(dev);
if (dd < 0) {
perror("Can't open device");
return -1;
}
if (hci_devinfo(dev, &di) < 0) {
perror("Can't get device info");
return -1;
}
opt = hci_test_bit(HCI_RAW, &di.flags);
if (ioctl(dd, HCISETRAW, opt) < 0) {
if (errno == EACCES) {
perror("Can't access device");
return -1;
}
}
hci_close_dev(dd);
}

/* Create HCI socket */


// Crea un socket, no orientado a conexion, de la familia de direcciones
// Bluetooth, que usara el protocolo HCI
sk = socket(AF_BLUETOOTH, SOCK_RAW, BTPROTO_HCI);
if (sk < 0) {
perror("Can't create raw socket");
return -1;
}

opt = 1;
if (setsockopt(sk, SOL_HCI, HCI_DATA_DIR, &opt, sizeof(opt)) < 0) {
perror("Can't enable data direction info");
return -1;
}

opt = 1;
if (setsockopt(sk, SOL_HCI, HCI_TIME_STAMP, &opt, sizeof(opt)) < 0) {
perror("Can't enable time stamp");
return -1;
}

/* Setup filter */
hci_filter_clear(&flt);
hci_filter_all_ptypes(&flt);
hci_filter_all_events(&flt);
if (setsockopt(sk, SOL_HCI, HCI_FILTER, &flt, sizeof(flt)) < 0) {
perror("Can't set filter");
return -1;
}

/* Bind socket to the HCI device */


addr.hci_family = AF_BLUETOOTH;
addr.hci_dev = dev;
// Liga el socket con una direccion bluetooth
if (bind(sk, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
printf("Can't attach to device hci%d. %s(%d)\n",
dev, strerror(errno), errno);
return -1;
}

return sk;
}

static void usage(void)


{
printf(
"Usage: hcidump [OPTION...] [filter]\n"
" -x, --hex Dump data in hex\n"
" -R, --raw Dump raw data\n"
" -h, --help Give this help list\n"
" --usage Give a short usage message\n"
);
}
static struct option main_options[] = {
{ "hex", 0, 0, 'x' },
{ "raw", 0, 0, 'R' },
{ "help", 0, 0, 'h' },
{ 0 }
};

int main(int argc, char *argv[])


{
unsigned long flags = 0;
unsigned long filter = ~0L;
int device = 0;
int defpsm = 0;
int defcompid = DEFAULT_COMPID;
int opt, pppdump_fd = -1, audio_fd = -1;

printf("HCI sniffer - Bluetooth packet analyzer ver %s\n", VERSION);

// recupera las opciones con las que se llamo el programa


while ((opt=getopt_long(argc, argv, "xXRh", main_options, NULL)) != -1) {
switch(opt) {
case 'x': // si la opcion fue 'x'
flags |= DUMP_HEX; // muestra los datos en formato hexadecimal
break;
case 'R': // si la opcion fue 'R'
flags |= DUMP_RAW; // muestra los datos sin formato
break;
case 'h':
default:
usage();
exit(0);
}
}

argc -= optind;
argv += optind;
optind = 0;

init_parser(flags, filter, defpsm, defcompid, pppdump_fd, audio_fd);


// Procesa los frames de bletooth que se reciben en la inteface hci0
// provenientes del Wiimote.
process_frames(device, open_socket(device, flags), -1, flags);

return 0;
}

Referencias
Lee, Johnny. Wii. http://www.cs.cmu.edu/~johnny/projects/wii/
CWiid. http://abstrakraft.org/cwiid
Wikipedia. Wiimote. http://en.wikipedia.org/wiki/Wiimote
FAQ:
Error: No se encuentra el archivo Python.h
Solución: Instalar el paquete python-dev (ubuntu) ó python-devel (fedora)
Nota: Instalar la versión de python mas reciente.

Laboratorio
En sintensis, se manejaran dos librerias distintas, CWiid y hciwiidump, ambas para el
desarrollo de la práctica. La primera se trata de la libreria para manipulación del control
del Wii y la segunda es un sniffer - analizador de tráfico - pensado para el Wiimote

Librería CWiid.

Prerequisitos

1) Asegurarse de instalar los pre-requisitos de software para la librería


cwiid:

Esto implica la instalación de los siguientes paquetes (a partir de los


repositorios oficiales de su distribución), sigue las instrucciones siguientes
para tu distribución de Linux:

Laboratorio de Sistemas Operativos. Dr. Juan Arturo Nolazco. 2009.


[1]

libbluetooth2(o superior )
bluez-utils
original-awk
bison
flex
libbluetooth2-dev
autoconf
mouseemu
libgtk2.0-dev
python (Para desarrollo en C - dev - )
make
gcc
Esto puede hacerse de manera gráfica vía un administrador de paquetes
(estilo Synaptics) o mediante la línea de comandos (un solo comando en
modo súper-usuario):

Ejemplos para Fedora:

su
yum install flex bison
yum install libbluetooth-dev
yum install libgtk2.0-dev
yum install cwiid lswm wmgui wminput
yum install bluez-libs bluez-libs-devel gtk2-devel
yum install python python-devel

ejemplos para Ubuntu:

sudo apt-get install flex


sudo apt-get install bison
sudo apt-get install libbluetooth-dev
sudo apt-get install libgtk2.0-dev
sudo apt-get install bluez-libs bluez-libs-devel gtk2-devel
sudo apt-get install python python-dev python-all python-all-dev

Es importante que recuerden, que las lineas de arriba son meramente


EJEMPLOS, los paquetes pueden haber sido actualizados despues de que fuera
escrita la práctica. Se les aconseja que buquen primero los paquetes relacionados,
para hacerlo lo pueden hacer por medio de las aplicaciones o de la terminal.

Ejemplos para Fedora:

su
yum search python
yum search bluez
yum search libbluetooth

ejemplos para Ubuntu:

sudo apt-cache gencaches


sudo apt-cache search python
sudo apt-cache search bluez
sudo apt-cache search libbluetooth

2) Descargar a la computadora el código fuente de la libreria cwiid:


Visitar la página http://www.cwiid.org y descargar la versión más reciente
de la librería (Actualmente la versión 0.6.00). Esto puede hacerse desde la
línea de comandos:

wget http://abstrakraft.org/cwiid/downloads/cwiid-0.6.00.tgz

3) Expandir el archivo que contiene el código fuente de la libreria


cwiid:

tar -zxvf cwiid-0.6.00.tgz

4) Compilar e instalar la librería cwiid desde su directorio principal:

ADVERTENCIA: El código bluetooth.c en lbcwiid usa una funcion


ya en obsoleta (deprecated) denominada "hci_remote_*" debe ser cambiada
manualmente a "hci_read_remote_*" en caso contrario no se podra compilar. (*
Significa cualquier nombre de esa funcion)

En una ventana de terminal, se compilara e instalara la lbirería, esto como


toda las demás librerias en un sistema Linux puede instalarse de la
siguiente manera :

cd cwiid-0.6.00/
./configure
make
sudo make install

Si no ha habido ningún error en la compilación e instalación, ya debe estar


disponible la librería cwiid y el programa de demostración gráfico
wmgui.

Nota: Si su instalación no le permite ejecutar comandos sudo, introducir el


comando su (sin más parámetros) y luego que pida el password introducir
el resto. No se olvide de salir del modo súper-usuario al finalizar la
ejecución del comando deseado.

Comprobar el correcto funcionamiento de la librería cwiid.

5) Compilar la demostración en modo texto del uso de cwiid que se


encuentra en el subdirectorio cwiid-0.6.00/wmdemo, mediante el uso de la
terminal a partir del directorio cwiid-0.6.00. Notar el uso del parámetro -l para
indicarle al gcc que enlace con la librería cwiid:

cd wmdemo
gcc -o wmdemo wmdemo.c -lcwiid

Si todo está correcto, debe generarse un archivo ejecutable wmdemo, el


cual se prueba con el comando:
./wmdemo

Y a continuación se siguen las instrucciones que aparecen en pantalla para


la demostración textual de la librería. El código fuente en wmdemo.c puede
servir de orientación para la programación empleando la API definida por
cwiid.

6) Ejecutar la demostración gráfica de cwiid, ejecutando desde


cualquier ventana de terminal el comando:

wmgui

Y seguir las instrucciones que aparecen en los menús respectivos (primer


paso: conectarse al dispositivo wiimote, etc…).

7) Conexión a un dispositivo en específico.

Al ejecutar las demostraciones indicadas en los pasos 5 y 6, se captará el


primer dispositivo wiimote capturado por el protocolo Bluetooth. Para
especificar directamente un dispositivo en particular, en el caso de que haya
varios controles cerca, debe primero habilitarse el wiimote en modo de
reconocimiento (apretando simultáneamente los botones 1 y 2) y
capturarse inmediatamente la dirección física del dispositivo que queremos
acceder, lo cual se realiza con el comando:

hcitool scan

el cual devolverá una salida similar a:

Scanning:
<dirección> Nintendo RVL-CNT-01

Siendo <dirección> el parámetro necesario a especificar, por ejemplo:

wmgui <dirección>
./wmdemo <dirección>

Puedes revisar un tutorial para usar el control Wii en Ubuntu en la página:


https://help.ubuntu.com/community/CWiiD

Cliente-Servidor Wiimote.

OBTENER INFORMACION DE LOS BOTONES DEL WIIMOTE:

En esta sección leerás el estado de cada uno de los botones del Wiimote, primero
crearás una conexión con el control mediante un driver HID y después “vaciarás” el
contenido de la información que el control le manda al driver.
1 - Cambiar a súper usuario (fedora) o ejecutar con sudo (Ubuntu).
2 - Buscar el dispositivo y obtener su direccion(bdaddr) :
Presionar simultáneamente los botones 1 y 2 del wiimote.
[root@localhost bluez-hcidump-1.42]# hcitool scan
--> 00:19:1D:BC:3F:07 Nintendo RVL-CNT-01
3 - Conectarse al dispositivo mediante un driver HID:
Sintaxis: hidd --connect <bdaddr>
Presionar simultáneamente los botones 1 y 2 del wiimote.
hidd --connect 00:19:1D:BC:3F:07
4 - Descarga el archivo hciwiidump.tar.gz . (Comando Wget)
5 - Descomprime el archivo
tar -xvzf hciwiidump.tar.gz
6 - Entra al directorio expandido.
7 - Entra al subdirectorio src .
8 - Pega el código del Anexo B (hciwiidump.c) dentro del directorio src en un
archivo con nombre hcidump.c . El código del Anexo B, es una versión reducida, de
la aplicación hcidump original, con el fin de mostrar claramente el uso de los
sockets.
9- Ejecute un make en la carpeta scr , si aparece un error parecido a la leyenda "
hcidump.c:52:24: error: redefinition of ‘ntoh64’" Abra el archivo hcidump.c y
comente la estructura ntoh64 y vuelva a ejecutar el make

9 - Regresa al directorio anterior (sube un directorio).


10 - Compila. (MAKE)
[root@localhost bluez-hcidump-1.42]# make

11 - Revisa que no aparezca algún error al terminar la compilación.
12 - Hacer dump de la información que contiene el driver HID del bluetooth:
[root@localhost bluez-hcidump-1.42]# ./src/hcidump -R
13 - En la pantalla se muestra la salida al presionar algún botón, por ejemplo al
presionar el boton B obtenemos la siguiente salida:
[root@localhost bluez-hcidump-1.42]# src/hcidump -R
HCI sniffer - Bluetooth packet analyzer ver 1.41
device: hci0 snap_len: 1028 filter: 0xffffffff
> 02 0B 20 08 00 04 00 41 00 A1 30 00 04
> 02 0B 20 08 00 04 00 41 00 A1 30 00 00
14 - Para salir del programa hcidump presionar CTRL + C
15 - Para terminar la conexión con el driver HID:
Sintaxis: hidd --kill <bdaddr>
hidd --kill 00:19:1D:BC:3F:07

Conectarse a una PC por Bluetooth y hacer "dump" de un "ping"


(l2ping)

En esta parte harás un “volcado” de la información que se intercambian una


computadora y un control Wiimote al hacerle un ping al mismo.

1 - Cambiar a súper usuario.


2 - Buscar el dispositivo y obtener su dirección(bdaddr) :
Asegurarse de que el bluetooth este en modo VISIBLE en la PC a la que se desea
conectar (remota).
$ hcitool scan
--> 00:16:38:C9:E7:08 MacBook
3 - Conectarse al dispositivo mediante un driver HID:
Sintaxis: hidd --connect <bdaddr>
hidd --connect 00:16:38:C9:E7:08
No hay problema si sale un mensaje como este:
Can't get device information: Success
4 - Hacer dump de la información que contiene el driver HID del bluetooth:
hcidump -R
5 - Abrir una nueva consola y cambiar a súper usuario.
Obtener la dirección (bdaddr) de nuestro adaptador bluetooth:
hci0: Type: USB
BD Address: 00:1E:37:DD:9F:3E ACL MTU: 1017:8 SCO MTU: 64:8
UP RUNNING PSCAN ISCAN
RX bytes:13050 acl:258 sco:0 events:378 errors:0
TX bytes:4761 acl:209 sco:0 commands:79 errors:0
Cerrar esta consola.
6 - Desde la computadora REMOTA hacer ping, mediante el protocolo L2CAP, a
nuestra computadora usando el comando:
Sintaxis: l2ping <bdaddr>
l2ping 00:1E:37:DD:9F:3E
Ping 00:1E:37:DD:9F:3E from 00:16:38:C9:E7:08 (data size 44)
44 bytes from 00:1E:37:DD:9F:3E id 0 time 14.42ms
44 bytes from 00:1E:37:DD:9F:3E id 1 time 14.42ms
44 bytes from 00:1E:37:DD:9F:3E id 2 time 14.42ms
44 bytes from 00:1E:37:DD:9F:3E id 3 time 14.42ms
Para terminar este programa presionar CTRL + C
7 - En la pantalla donde corre el hcidump obtenemos la siguiente salida:
[root@localhost wiimote]# hcidump -R
HCI sniffer - Bluetooth packet analyzer ver 1.41
device: hci0 snap_len: 1028 filter: 0xffffffff
> 04 04 0A 08 E7 C9 38 16 00 0C 21 4A 01
< 01 09 04 07 08 E7 C9 38 16 00 00
> 04 0F 04 00 01 09 04
> 04 12 08 00 08 E7 C9 38 16 00 00
> 04 03 0B 00 0B 00 08 E7 C9 38 16 00 01 00
< 02 0B 20 0A 00 06 00 01 00 0A 01 02 00 02 00
< 01 1B 04 02 0B 00
> 04 0F 04 00 01 1B 04
< 01 0D 08 04 0B 00 0F 00
> 04 0B 0B 00 0B 00 FF FF 8D FE 9B F9 00 80
> 04 0E 06 01 0D 08 00 0B 00
< 01 0F 04 04 0B 00 18 CC
> 04 0F 04 00 01 0F 04
< 01 19 04 0A 08 E7 C9 38 16 00 02 00 00 00
> 04 1D 05 00 0B 00 18 CC
> 04 0F 04 00 01 19 04
> 04 1B 03 0B 00 05
> 02 0B 20 10 00 0C 00 01 00 0B 01 08 00 02 00 00 00 00 00 00
00
> 02 0B 20 34 00 30 00 01 00 08 C8 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
< 02 0B 20 34 00 30 00 01 00 09 C8 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
> 04 07 FF 00 08 E7 C9 38 16 00 6C 6F 63 61 6C 68 6F 73 74 2E
6C 6F 63 61 6C 64 6F 6D 61 69 6E 2D 30 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
> 04 13 05 01 0B 00 02 00
> 02 0B 20 34 00 30 00 01 00 08 C9 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
< 02 0B 20 34 00 30 00 01 00 09 C9 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
> 04 13 05 01 0B 00 01 00
> 02 0B 20 34 00 30 00 01 00 08 CA 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
< 02 0B 20 34 00 30 00 01 00 09 CA 2C 00 41 42 43 44 45 46 47
48 49 4A 4B 4C 4D 4E 4F 50 51 52 53 54 55 56 57 58 59 5A 5B
5C 5D 5E 5F 60 61 62 63 64 65 66 67 68 41 42 43 44
> 04 13 05 01 0B 00 01 00
Alterna
7B - Alternativa a dos computadoras se puede utilizar tambien el wiimote, pero no
se tratara de un ping si no de la informacion que envía el wiimote
8 - Para salir del programa hcidump presionar CTRL + C
9 - Para terminar la conexión con el driver HID:
Sintaxis: hidd --kill <bdaddr>
hidd --kill 00:16:38:C9:E7:08

Reporte segun lo que se le pida en el formulario


Laboratorio Acceso al laboratorio

Laboratorio de Sistemas Operativos


Práctica 10: Vulnerabilidades

Seguridad informática: Vulnerabilidades, Amenazas y Ataques.


Exploración de puertos,Exploración de Vulnerabilidades

Autor: Dr. Juan A. Nolazco


Co-Autor: Ing. Raúl Fuentes Samaniego

ÍNDICE
1. Seguridad en informática.
1. Vulnerabilidades, Amenazas, Ataques y Controles.
2. Programación No segura
1. ¿Y esto en qué me afecta?
3. Tipos de Riesgos en un sistema
1. Errores de Diseño, Implementación y Operación
2. ¿Que busca un atacante de mi equipo de computo?
3. Los recursos pueden ser de diversas índoles pero los principales son:
4. ¿Qué son las botnets?
5. ¿Cómo defenderse de estos Ataques?
2. Nmap.
1. Sintáxis
2. Ejemplos.
3. Nessus
1. Instalación de Nessus 5
4. FAQ:
5. Referencias:
6. Laboratorio:
1. Nmap.
2. Nessus

Seguridad en informática.
Cualquier parte de un sistema computacional puede ser el objetivo de un crimen. Un
sistema computacional se puede ver como una colección de hardware, software,
medio de almacenamiento, datos y personas que una organización usa para realizar
una tarea de computo. A veces, asumimos que las partes de un sistema
computacional no tienen valor para un intruso, pero a menudo nos equivocamos. Por
ejemplo, tendemos a pensar que los bienes más valiosos en un banco son el dinero, el
oro y la plata en la bóveda. Pero de hecho la información del cliente dentro de la
computadora del banco puede ser más valiosa. Almacenada en papel, grabada en un
medio de almacenamiento, residente en memoria, o transmitida por la líneas de
teléfono o por enlaces satelitales, esta información puede ser usada en miles de
formas para ganar dinero de forma ilícita. Un banco de la competencia podría usar
esta información para robar clientes o incluso interrumpir el servicio y desprestigiar al
banco. Un individuo sin escrúpulos podría transferir dinero de una cuenta a otra sin el
permiso del propietario. Un grupo de estafadores podría contactar una gran número de
depositantes y convencerlos de invertir en planes fraudulentos. La variedad de
objetivos y ataques vuelve la seguridad informática muy difícil.

Cualquier sistema es más vulnerable en su punto más débil. Un ladrón que intente
robar algo de tu casa no intentará penetrar una puerta metálica de cinco centímetros
de ancho si una ventana le da un acceso más fácil. Del mismo modo, un sistema
sofisticado de seguridad física perimetral no compensa el acceso sin vigilancia a
través de una simple línea telefónica y un MODEM. Esta idea es uno de los principios
de la seguridad computacional.

Principio de la penetración más fácil. Se debe esperar que un intruso utilice


cualquier medio de penetración disponible. La penetración no
necesariamente será por el medio más obvio, ni es necesariamente contra
aquel que tiene la defensa instalada más sólida. Y no tiene que ser en la forma
en que esperamos que se comporte el atacante.

Este principio implica que un especialista en seguridad computacional debe considerar


todos los medios de penetración posibles. Más aún, el análisis de penetración debe
ser hecho continuamente y especialmente cuando el sistema y su seguridad cambien.
Recuerda que la seguridad computacional es un juego que solo tiene reglas para el
equipo defensivo: los atacantes pueden (y lo harán) usar cualquier medio que puedan.
Tal vez lo más difícil para las personas fuera de la comunidad de seguridad
es pensar como el atacante. Un grupo creativo de investigadores de seguridad,
investigó un sistema de seguridad inalámbrica y reportó una vulnerabilidad al jefe de
diseño del sistema, quien respondió “que funcionaria, pero ningún atacante lo
intentaría”. No pienses eso ni por un minuto: ningún ataque está fuera de límites.

Vulnerabilidades, Amenazas, Ataques y Controles.


Un sistema basado en computadora tiene tres componentes distintos pero valiosos:
hardware, software y datos. Cada uno de estos activos ofrece valor a los diferentes
miembros de la comunidad afectada por el sistema. Para analizar la seguridad,
podemos realizar una lluvia de ideas sobre las formas en las que el sistema o su
información puede experimentar algún tipo de pérdida o daño. Por ejemplo, podemos
identificar datos cuyo formato o contenido debería ser protegido de alguna forma.
Queremos que nuestro sistema de seguridad asegure que ningún dato es divulgado a
partes no autorizadas o se realicen modificaciones ilegítimas, así mismo asegurarse
que los usuarios legítimos tienen acceso a los datos.
Una vulnerabilidad es una debilidad en el sistema de seguridad, puede ser: en
procedimientos, diseño o implementación, que podría ser explotada para causar
pérdida o daño. Por ejemplo, un sistema particular podría ser vulnerable a la
manipulación de datos no autorizada debido a que el sistema no verifica la identidad
de un usuario antes de permitirle el acceso a los datos.

Una amenaza a un sistema computacional es un conjunto de circunstancias que


tienen el potencial de causar una pérdida o daño.

Para ver la diferencia entre una amenaza y una vulnerabilidad vea la Figura 1; en ella
se puede ver una pared conteniendo el agua. El agua a la izquierda de la pared es una
amenaza para el hombre a la derecha de la pared; el agua podría aumentar de nivel,
inundando al hombre, o podría permanecer por debajo de la altura de la pared y
causar que la pared collapse. Así que la amenaza de daño es el potencial para el
hombre de mojarse, lastimarse o ser ahogado. Por ahora, la pared está intacta, por lo
que la amenaza para el hombre no está realizada.

Sin embargo, podemos ver una pequeña fractura en la pared, una vulnerabilidad que
amenaza la seguridad de la persona. Si el agua se eleva al nivel (o más allá) de la
fractura, explotará la vulnerabilidad y dañará a la persona.

Figura 1 - Riesgo, Vulnerabilidad y amenaza

Un humano que explota (o aprovecha) una vulnerabilidad perpetra un ataque al


sistema. Un ataque también puede ser iniciado por otro sistema, como cuando un
sistema envía a otro un abrumador conjunto de mensajes o mensajes incompletos
para provocar incongruencias en los procesos, virtualmente deteniendo la habilidad de
funcionar del segundo. Desafortunadamente se ha visto este tipo de ataque
frecuentemente; se conoce como denegación de servicio (DoS).

¿Cómo podemos abordar estos problemas? Usamos un control como una medida
de protección. Un control es una acción, un dispositivo, un procedimiento o una técnica
que remueve o reduce una vulnerabilidad. En la Figura 1, la persona puede poner un
dedo en un hoyo y así controlar la amenaza de que el agua se fugue, hasta que
encuentre una solución al problema más permanente. En general, podemos describir
la relación entre amenazas, controles y vulnerabilidades de esta forma: Una amenaza
se bloquea por un control de una vulnerabilidad.
Los daños ocurren cuando una amenaza se lleva a cabo contra una vulnerabilidad.
Para proteger contra el daño, entonces, podemos neutralizar la amenaza, cerrar la
vulnerabilidad, o ambos. La posibilidad de que un daño ocurra se conoce como
riesgo. Podemos hacer frente a los daños de diferentes maneras. Podemos tratar de:
 Prevenir, bloquear el ataque o cerrar la vulnerabilidad,
 Disuadir, hacer el ataque más difícil pero no imposible,
 Desviar, volver otro objetivo más atractivo, o este no tanto,
 Detectar, cuando sucede o algún tiempo después,
 Recuperar de sus efectos.

Figura 3 - No siempre es posible evitar una vulnerabilidad, pero si reducir la amenaza


para controlar el riesgo.

Programación NO segura
Aprender a programar es un arte, pero saber programar con metodologías y auditorias
de seguridad es un reto que debe ser obligatorio en nuestros días y sin embargo
sigue teniendo baja penetración. Como una comprobación del actual estado, el
instituto ISACA (www.isaca.org ) realizó una encuesta a inicio del 2012 a mas de 20
mil desarrolladores de aplicaciones y mas de 6 mil auditores de seguridad obteniendo
resultados interesantes, tales como:
 79% De los desarrolladores no manejan ningún esquema de seguridad en sus
códigos o bien manejan esquemas ad-hoc para la creación de aplicaciones.
 Ambos grupos opinan fuertemente que el modelo de construcción de software
SDLC (System Development Life Cycle) no incluye fases específicamente para
validar la seguridad.
 30% de los desarrolladores crean o aplican los modelos de seguridad una vez
que la aplicación ha sido publicada.
 51% de los desarrolladores consideran que no estan entrenados para detectar
y/o corregir fallas de seguridad en el código de las aplicaciones.
 Más de la mitad de los desarrolladores consideran que reparar bugs o parchar
los programas es un consumo de recursos de la empresa desperdiciado.
 Una parte significativa de las auditorías de seguridad se enfocan a la
comunicación entre aplicaciones y no propiamente a la capa Aplicación.

En síntesis, un desarrollador de sistemas, aplicaciones o componentes debe tomar en


cuenta una gran variedad de elementos antes de liberar el código; de hecho, asignar
esta tarea a un solo desarrollador es garantía de eventual fracaso.

En SANS.org se maneja un listado de los errores mas severos que puede sufrir el
código fuente de una aplicación (http://www.sans.org/top25-software-errors/ ). Dicho
listado es actualizado cada año para tomar en cuenta aquellos elementos que están
siendo más explotados. Investiga en la web en qué consiste cada uno que no
conozcas:

• Interacción entre componentes de forma insegura: Se refiere al paso de


información de un módulo a otro (incluyendo humano-máquina).

 Improper Neutralization of Special Elements used in an SQL Command


('SQL Injection')
 Improper Neutralization of Special Elements used in an OS Command
('OS Command Injection')
 Improper Neutralization of Input During Web Page Generation ('Cross-site
Scripting')
 Unrestricted Upload of File with Dangerous Type
 Cross-Site Request Forgery (CSRF)
 URL Redirection to Untrusted Site ('Open Redirect')

• Manejo de recursos de forma riesgosa - Relaciona las amenazas cuando el


software no maneja de forma adecuada la creación, uso, transferencia de recursos
importantes del sistema.

 Buffer Copy without Checking Size of Input ('Classic Buffer Overflow')


 Improper Limitation of a Pathname to a Restricted Directory ('Path
Traversal')
 Download of Code Without Integrity Check
 Inclusion of Functionality from Untrusted Control Sphere
 Use of Potentially Dangerous Function
 Incorrect Calculation of Buffer Size
 Uncontrolled Format String
 Integer Overflow or Wraparound

• Defensas porosas - Técnicas de defensa que no están bien empleadas, se abusa


de ellas o simplemente son ignoradas.
 Missing Authentication for Critical Function
 Missing Authorization
 Use of Hard-coded Credentials
 Missing Encryption of Sensitive Data
 Reliance on Untrusted Inputs in a Security Decision
 Execution with Unnecessary Privileges
 Incorrect Authorization
 Incorrect Permission Assignment for Critical Resource
 Use of a Broken or Risky Cryptographic Algorithm
 Improper Restriction of Excessive Authentication Attempts
 Use of a One-Way Hash without a Salt

Se recomienda a todo programador auditar sus códigos mientras están en fase de


desarrollo y tomar fuertes precauciones particularmente a Buffer Overflow. Aunque
actualmente esta en el tercer lugar, este problema ha permanecido desde un inicio en
el Top5 y solo fue desbancado por SQL Injection por la alta penetración que han
tenido las DB en los sistemas modernos. De hecho, ambos van exactamente por el
mismo problema: Entradas no vigiladas que pueden permitir la ejecución de código arbitrario.

Una herramienta adicional es el listado recientemente publicado por ISC es Integrating


Software Security Into The SDLC, el cual mantiene un listado de las funciones que son
inherentemente inseguras y se deben tomar consideraciones especiales para su uso.
Esta colección abarca diferentes lenguajes de programación tales como: C/C++,
ASP/.NET, Java, Perl, PHP, Python y ColdFusion.

¿Y en que me afecta en el desarrollo de mis aplicaciones?


Existen distintas entidades que por diferentes razones puedan necesitar obtener cierta
información; para ello pueden aprovechar mecanismos conocidos o los menos
conocidos. Las razones de la necesidad pueden ser variados; algunos serán legales,
otros éticamente cuestionables y algunos definitivamente ilegales, pero sin importar la
razón el puro hecho de existir la necesidad hace que exista un mercado. Pues bien,
este mercado obtiene grandes cifras de dinero a cambio de los "exploits" -
penetraciones causadas por vulnerabilidades.

Un excelente ejemplo ha ocurrido en el evento Pwn2Own de Marzo 2012. El grupo


francés Vupen Security consiguió por primera vez romper completamente la seguridad
del navegador Chrome de Google y ejecutar código arbitrario con las credenciales del
usuario. El detalle vino cuando Google intentó comprar el exploit a Vupen por 60 mil
dlrs (en realidad, la cantidad prometida como premio en el evento ). Sin
embargo, Vupen se ha guardado el exploit para ponerlo en venta al mejor postor con
estas palabras: "No compartiríamos esto con Google ni por USD$ 1 millón. No
queremos darles ningún conocimiento que les ayude a reparar este exploit u otros
exploits similares. Queremos dejar esto para nuestros clientes" (Pueden leer este
artículo de Forbes para más información).

¿Qué tan normal es el escenario anterior en estos días? Bastante normal; los exploits
menos conocidos y por lo tanto que más probabilidad tienen de tener éxito se pueden
cotizar en miles de dólares. Otro caso conocido es el de el gobierno estadounidense
que pagó un cuarto de millón de dolares por un exploit en iOS para los teléfonos
inteligentes Iphone. Estas ventas se hacen por intermediarios , pero de acuerdo a
Forbes la siguiente tabla (Fig. 4) muestra un estimado de precios actuales (2012).
Figura 4 - Precios en dólares de venta de vulnerabilidades no conocidas

Tipos de Riesgos en un sistema


Cuando alguien piensa en un ataque al sistema puede pensar en robar documentos o
en la inyección de virus, incluso recordará los eventos que se vivieron en el 2011 tales
como: Ataques a plantas nucleares (Stuxnet), el fiasco de seguridad de Sony, el robo
de certificados SSL a Comodo y RSA entre otros problemas graves. El intentar entrar
en detalle a todos estos riesgos y amenazas es demasiado para una sola sesión y por
lo mismo nos enfocaremos exclusivamente a amenazas internas en un solo
dispositivo, pero debe quedar en claro, que esto apenas es la punta del Iceberg.

Figura 5 - Los ataques modernos no son meramente hechos por "aburrimiento" pero si
se intentan volver "sencillos"

El siguiente listado son las técnicas con las cuales se puede atacar y/o espiar a una
máquina en particular.

1. Correo electrónico - una vía de acceso fácil para virus y caballos de troya, ya
sea que viajen ocultos en el correo o bien infectan archivos adjuntos de los
correos. El riesgo de contagio viene que el programa para leer los correos debe
abrir el correo y entonces puede ocurrir la infección. La mejor técnica es el
empleo de antivirus y firewalls, además del sentido común de no abrir correos
sospechosos.
2. Navegación por servidores web - Se puede llegar a páginas que utilicen
código Java o controles ActiveX, los cuales son muy empleados para la creación de
código malicioso que se ejecutan directamente desde el equipo atacado. Por
supuesto, la descarga de software contaminado (código original con porciones
maliciosas embebidas a él) afectan de forma similar al caso 1.

3. Eavesdroping (intercepción pasiva) - Se trata de capturar todo el tráfico de


red que viene a la máquina o sale de la máquina. Cualquier dispositivo dentro de la red
que comparta el mismo medio lo puede realizar, incluso algunos malware dentro del
mismo equipo. El objetivo es robar la información que el usuario está transmitiendo,
por ejemplo: números de tarjetas de crédito, logins y contraseñas entre otras cosas.

4. Snooping (espionaje de información) - Propiamente dicho va un paso


más que el caso 3 al realizarse también búsqueda de información dentro del
sistema, tales como documentos, correos, lista de contacto y llaves maestras. El
snooping suele hacerse con fines de espionaje y robo de información.

5. Tampering (modificación de la información) - Mediante este tipo de


ataque se pueden editar ciertos tipos de archivos dentro de nuestras maquinas
sin autorización. Esta modificación puede incluir el borrado de archivos. Si el
atacante logra obtener suficientes privilegios entonces todo el sistema está a
merced del ataque. El simple cambio de imágenes web en un portal es un
ejemplo de estas amenazas.

6. Envío de mensajes con nuestra identidad - se trata del uso de


nuestros correos por terceros. Los datos se pudieron haber obtenido por
ingeniería social o algún ataque de los ya indicados. Generalmente el objetivo
es infectar a otros usuarios, dándole validez al archivo infectado al venir de una
fuente confiable, o incluso utilizar nuestras credenciales para actividades ilícitas.
Esto es igualmente aplicable a nuestras cuentas de redes sociales.

7. Spoofing - Consiste en tomar la identidad de otro al tener una


comunicación. Generalmente la comunicación es iniciada de forma real (por
quienes se desean comunicar) pero en algún punto un tercero se hace pasar por
uno de los originales (ya sea sin haber afectado a uno o después de haberlo
eliminado de la comunicación). Aunque a nuestras computadoras esto puede no
parecer peligroso si lo es, si el lado afectado es por ejemplo, el banco al que
queremos conectarnos para iniciar nuestras transferencias bancarias.
Errores de Diseño, Implementación y Operación
Muchos sistemas están expuestos a "agujeros" de seguridad que son explotados para
acceder a archivos, obtener privilegios o realizar sabotaje. Estas vulnerabilidades
ocurren por variadas razones, y miles de "puertas invisibles" son descubiertas (cada
día) en sistemas operativos, aplicaciones de software, protocolos de red, browsers de
Internet, correo electrónico y todas clase de servicios informático disponible.

Los Sistemas operativos abiertos (como GNU/Linux) tienen agujeros mas conocidos y
controlados que aquellos que existen en sistemas operativos cerrados (como
Windows, MAC OS X, iOS). La importancia (y ventaja) del código abierto radica en
miles de usuarios que analizan dicho código en busca de posibles bugs y ayudan a
obtener soluciones en forma inmediata.

Mensaje de los días Modernos al pasado: NAH! Los hackers de grupos


como Vupen son una tendencia moderna y provienen justamente de aquellos
que dedicaban su tiempo libre en buscar dichos errores por amor al arte, pero
ahora en vez de amor al arte es amor al dinero. La alta diversidad de
distribuciones provoca que ciertos S.O. desvía atención de los hackers de
sombrero blanco solo o los obliga a dividir sus esfuerzos haciendo que
vulnerabilidades conocidas puedan estar corregidas en unos S.O. pero no en
otros. COmo ejemplo, Fedora no actualiza el kernel de Linux y mientras que
Ubuntu puede estar a salvo de exploits bien conocidos a SUDO en Kernel
3.2 el Fedora del 2013 sigue siendo vulnerable (al menos que el usuario
compile el nuevo kernel).

Constantemente encontramos en Internet avisos de nuevos descubrimientos de


problemas de seguridad (y herramientas de Hacking que los explotan), por lo que hoy
también se hace indispensable contar con productos que conocen esas debilidades,
puedan diagnosticarlas y actualizar el programa afectado con el parche adecuado.

Es importante recalcar que es un mito decir que un GNU/Linux, Android o un


sistema MAC OS X, iOSx están libre de virus y ataques maliciosos. Tal vez el
virus no pueda dañar el sistema de archivo de ellos a primera vista, tal vez no pueda
ejecutar "rm -r /" PERO si puede leer y/o modificar todos los documentos del usuario
o matar procesos del usuario al que ha conseguido infectar además de hacer que el
usuario le de los privilegios que ocupa para hacer mayor daño (Algo extremadamente
común en estos días). La siguiente imagen demuestra la cantidad de malware que fue
identificado como código único durante el 2011 solamente para los los sistemas de
Apple:

Imagen 6 - Sumario de Malware para MAC en el 2011

Se pueden apreciar puertas traseras, las cuales serían el primer paso para un ataque
escalonado. Los más importantes son los troyanos auto-descargables que tuvieron un
impacto en Noviembre. Entre los malwares se incluyen Flashback y Devilrobber.

En general, la única razón por la que estos sistemas tenían pocas amenazas en el
pasado era por su baja penetración en el mercado. En este aspecto Android es un
claro ejemplo. En el 2010 se tenía no más del 1% de los malware hacia dispositivos
móviles identificados para dicho S.O., pero para el 2011 Android paso a sufrir casi la
mitad del Malware en dichos medios, esto debido a su alta penetración en el mercado.
En síntesis, ningún S.O. es libre de vulnerabilidades y dependiendo de su uso es que
tanto se intenta explotarlas y en cuanto tiempo y de qué magnitud resultan las
explotaciones.

Figura 7 - Malware (versiones únicas) para móviles detectados por S.O. móvil

¿Qué busca un atacante de mi equipo de computo?


El considerar que no contamos con nada relevante o atractivo para sufrir un ataque
informático es una idea equivocada y peligrosa respecto a la seguridad. La confusión
tiene su origen en pensar que los atacantes o hackers quieren datos e información
nuestros y esto no siempre es cierto. La mayor parte de las veces, lo que necesitan
son recursos. Y esta distinción hace que cualquier usuario conectado a Internet, por el
simple hecho de estarlo, ya posea cierto valor para un hacker.

Los recursos pueden ser de diversas índoles pero los principales son:

 Dinero: El robo de dinero puede presentarse en muchas formas y en muchas de


ellas el hacker no necesita forzosamente robar sus cuentas bancarias para
obtener dinero de usted o para que su computadora le ayude a generar dinero.
Este podría por ejemplo realizar “bitcoin mining” que consiste en el robo de una
moneda electrónica que básicamente sólo circula en la red pero que puede
convertirse en dinero contante y sonante o por ejemplo si posee una cuenta de
Pay-pal, el atacante puede robar sus claves y quitarle el dinero que tenga en ella
o el dinero que quiera mover a través de ella, como pagar algo mediante su
cuenta. En el caso del robo o generación de bitcoins, el atacante haría uso del
procesador o del nivel y velocidad de procesamiento de su computadora, entre
mejor sea, más rápido podría efectuar el robo y de esa manera evitar ser
detectado.

 Ancho de banda: Consiste básicamente en que el atacante inserta un virus en


su computadora, generalmente un Troyano, el cual sin ser detectado, convierte
a su equipo en un zombie o esclavo del computador del atacante y realiza con
ayuda de su velocidad de internet, es decir, el ancho de banda de su
computadora, cualquier tarea que este desee, como por ejemplo, insertar el
mismo virus en otras computadoras y generar una cadena de zombies al
servicio de un mismo hacker.

 Procesador - Parecido al BW pero enfocados a los recursos que nuestros


sistemas poseen, esto usualmente esta combinado al dinero. Y por lo general
involucran Botnets.

 Contraseñas: Nos referimos a sus contraseñas de cuentas de correo o de las


redes sociales.

¿Qué son las botnets?

“Bot” es el diminutivo de la palabra “Robot”. Son pequeños programas que se


introducen en el ordenador por intrusos, con la intención de tomar el control remoto
del equipo del usuario sin su conocimiento ni consentimiento.

Las redes de bots o Botnet, es una red o grupo de ordenadores infectados por bots y
controlados remotamente por el propietario de los bots. Este propietario da
instrucciones que pueden incluir: la propia actualización del bot, la descarga de una
nueva amenaza, mostrar publicidad al usuario, el envío de spam o el lanzar ataques
de denegación de servicio, entre otras. Generalmente, al ordenador infectado se le
denomina zombi. Algunas redes pueden llegar a tener decenas de miles de
ordenadores zombis bajo control remoto.
Figura 8 - Botnet (Panda Security)

No asuma que los botnets son problemas solamente de PC, cualquier dispositivo en
Internet o una red grande (por los recursos que esta maneja) es un potencial zombie,
se han detectado Botnets en Android, Linux, Windows, PS3, Servidores WEB
legítimos, celulares vía SMS, y un sinfín de etc…

¿Cómo defenderse de estos Ataques?

La mayoría de los ataques mencionados se basan en fallos de diseño inherentes a


Internet (y sus protocolos) y a los sistemas operativos utilizados, por lo que no son
"solucionables" en un plazo breve de tiempo.

La solución más accesibles en cada caso es mantenerse informado sobre todos los
tipos de ataques existentes y las actualizaciones que permanentemente lanzan las
empresas desarrolladoras de software, principalmente de sistemas operativos. Las
siguientes son medidas preventivas que toda red y administrador deben conocer y
desplegar cuanto antes:

1. Mantener las máquinas actualizadas y seguras físicamente


2. Mantener personal especializado en cuestiones de seguridad (o subcontratarlo)
en caso de empresas.
3. Aunque una máquina no contenga información valiosa, hay que tener en cuenta
que puede resultar útil para un atacante, a la hora de ser empleada en un DoS
coordinado o para ocultar su verdadera dirección.
4. No permitir el tráfico "broadcast" desde fuera de nuestra red. De esta forma
evitamos ser empleados como "multiplicadores" durante un ataque Smurf.
5. Filtrar el tráfico IP Spoof.
6. Auditorías de seguridad y sistemas de detección.
7. Mantenerse informado constantemente sobre cada unas de las vulnerabilidades
encontradas y parches lanzados. Para esto es recomendable estar suscrito a
listas que brinden este servicio de información.
8. Por último, pero quizás lo más importante, la capacitación continua del
usuario.

Nmap.
Nmap es un escáner de seguridad, que permite descubrir computadoras y
servicios para crear un mapa de la red, es capaz de descubrir servicios pasivos,
aunque estos no se anuncien. Permite determinar varias características de los
sistemas remotos, como: el sistema operativo, el tipo de dispositivo, el tiempo de
actividad, el software que proporciona el servicio y su versión exacta, entre otras
características. Su página oficial es: Nmap

Las características de Nmap son:

 Descubrimiento de equipos, identificar computadoras en una red.


 Escaneo de puertos, enumerar los puertos abiertos en los equipos objetivo.
 Detección de versión, interrogar a los servicios remotos para determinar el
software y la versión.
 Detección de sistema operativo, determinar el sistema operativo y algunas
características de hardware.

Nmap puede ser utilizado para detectar vulnerabilidades en una red y prevenir que
éstas sean aprovechadas por alguna persona; los administradores de sistemas lo
utilizan generalmente para detectar servidores no autorizados y computadoras que no
cumplan con los requisitos mínimos de seguridad de una organización.

Instalación
Se puede utilizar apt para su instalación (apt-get install nmap) pero usualmente se
trata de versiones anteriores. Si decide instalar la ultima versión para Linux este
tutorial de nmap.org le será util.

Sintaxis
La sintaxis para ejecutar Nmap desde la línea de comandos es la siguiente:
nmap [ <tipo-de-búsqueda> ...] [ <opciones> ] { <especificación-del-
objetivo> }
dónde:

<tipo-de-búsqueda> especifica diferentes tipos de búsqueda, algunos de estos


son:
Descubrimiento de equipos:
-sL Lista de exploración, crea una lista de direcciones IP que
se van a escanear.
-sP Exploración por Ping, solo revisa si el equipo está en
línea.
Técnicas de búsqueda:
-{sS,sT,sA,sW,sM} Exploración: TCP SYN, Connect(), ACK,
Window, Maimon, respectivamente.
-sU Exploración UDP.
-{sN,sF,sX} Exploración TCP Null, FIN y Xmas.
Especificación de puerto:
-p <rango-de-puertos> Solo explora los puertos
especificados, por ejemplo: -p22; -p1; -p65535; -p
U:53,111,137,T:21-25,80,139,8080
-F Modo rápido, explora menos puertos que la exploración
estándar.
-r Explora puertos consecutivamente, no de forma aleatoria.
<opciones>
Detección de servicio y versión:
-sV Examina los puertos abiertos para determinar la
información del servicio y su versión.
Evasión y burlado de Firewalls e IDS(Intrusion Detection System):
-f;--mtu <val> Fragmentar los paquetes (opcionalmente con
el MTU especif.)
-D <señ1,señ2,…> Ocultar una exploración con señuelos.
-S <dir-IP> Falsificar la dirección fuente.
--spoof-mac <dir-MAC> Falsificar la dirección MAC.
--ip-options <opciones> Envía paquetes con las opciones IP
especificadas.
Detección de Sistema Operativo:
-O Habilita la detección del sistema operativo.
Salida:
-{oN,oX,oS,oG} <archivo> La salida de guarda en un archivo
con formato Normal, XML, Script kiddie o Grepable.
-v Aumenta el nivel de verbosidad.
Varios:
-6 Habilita la exploración en IPv6
-A Habilita la detección de sistema operativo y detección de
versión, búsqueda de script y Traceroute.
-h Imprime un resumen de ayuda.
<especificación-del-objetivo> Pueden ser los nombres de host, direcciones IP,
redes, etc. Por ejemplo:
scanme.nmap.org Explora la dirección IP
asociada a este dominio.
microsoft.com/24 Explora la subred clase C a la
que pertenece este dominio.
192.168.0.1 Explora esta dirección IP.
10.0.0-255.1-254 Explora de la dirección
10.0.0.1 a la 10.0.255.254, excepto aquellas cuyo cuarto
octeto sea igual a 0 ó 255, por ejemplo la dirección
10.0.0.255 no es explorada.

Otro tutorial que explica en más detalle algunas de estas opciones:


http://nmap.org/bennieston-tutorial/

Cuando se ejecuta Nmap desde la línea de comandos, todo lo que no es una opción (o
un argumento de opción) es tratado como una especificación de objetivo. El caso más
simple es especificar una dirección IP o un nombre de dominio a explorar.
En algunas ocasiones se desea explorar una red completa de equipos adyacentes.
Para esto, Nmap soporta direccionamiento estilo CIDR (Classless Inter-Domain
Routing). Esto se hace agregando: /<número-de-bits> a la dirección IP (o nombre de
dominio), y Nmap explorará cada dirección IP para la cual los primeros <numero-de-
bits> son los mismos para la dirección IP de referencia (o IP del nombre de dominio)
dada. Por ejemplo, si el objetivo es: 192.168.10.0/24 se explorarán los 256 equipos
cuyas direcciones IP están entre 192.168.10.0 y 192.168.10.255, inclusive;
192.168.10.49/24 explorará exactamente los mismos objetivos.

Nmap permite especificar una lista de números y rangos, separada por comas, para
cada octeto. Por ejemplo: 192.168.3-5,7.1 explorará las direcciones 192.168.3.1,
192.168.4.1, 192.168.5.1 y 192.168.7.1 .

Ejemplos.
nmap -v -A scanme.nmap.org
nmap -v -sP 192.168.0.0/16 10.0.0.0/8
nmap -v -iR 10000 -PN -p 80

La salida de Nmap es una lista de objetivos explorados, con información suplementaria


de cada uno dependiendo de las opciones usadas. La información clave de la salida
está contenida en la tabla “Puertos Interesantes”. Esta tabla muestra el número de
puerto y protocolo, el nombre del servicio, y el estado. El estado puede ser: open,
filtered, closed, o unfiltered. Open quiere decir que una aplicación en la máquina
objetivo está esperando conexiones o paquetes en ese puerto. Filtered quiere decir
que un firewall, filtro, u otro dispositivo de red está bloqueando ese puerto y Nmap no
puede decir si el puerto esta abierto o cerrado. Los puertos en estado closed no tienen
alguna aplicación escuchando en él, aunque se podrían abrir en algún momento.
Aquellos clasificados como unfiltered responden al sondeo de Nmap pero no se puede
determinar si están cerrados o abiertos.

El siguiente ejemplo muestra la ejecución de Nmap sobre el dominio


scanme.nmap.org:
# nmap -A -T4 scanme.nmap.org

Starting Nmap ( http://nmap.org )


Interesting ports on scanme.nmap.org (64.13.134.52):

Not shown: 994 filtered ports

PORT STATE SERVICE VERSION


22/tcp open ssh OpenSSH 4.3 (protocol 2.0)
25/tcp closed smtp
53/tcp open domain ISC BIND 9.3.4
70/tcp closed gopher
80/tcp open http Apache httpd 2.2.2 ((Fedora))
|_ HTML title: Go ahead and ScanMe!
113/tcp closed auth
Device type: general purpose
Running: Linux 2.6.X
OS details: Linux 2.6.20-1 (Fedora Core 5)

TRACEROUTE (using port 80/tcp)


HOP RTT ADDRESS
[Cut first seven hops for brevity]
8 10.59 so-4-2-0.mpr3.pao1.us.above.net (64.125.28.142)
9 11.00 metro0.sv.svcolo.com (208.185.168.173)
10 9.93 scanme.nmap.org (64.13.134.52)

Nmap done: 1 IP address (1 host up) scanned in 17.00 seconds

Los argumentos usados en este ejemplo son: -A, para habilitar la detección de sistema
operativo, exploración de script y traceroute; -T4 para una ejecución más rápida; y
también el nombre de dominio.

Los resultados arrojados nos indica la traducción de dominio a IP ( 64.13.134.52 ), nos


permite tambien ver que acepta conexiones seguras bajo SSH y que se trata de un
servidor Web Tomcat ejecutandose en una maquina con el S.O. Fedora version 5 y el
kernel 2 de Linux.

En general, si en el rango de direcciones a analizar están los servidores veremos los


puertos de escucha en funcionamiento (80, 22 en el ejemplo anterior). Si el puerto es
conocido, asumirá que se trata de ello y lo podrá confirmar de acuerdo al mensaje de
respuesta que reciba .

La instalación de nmap se puede logar desde el repositorio de Ubuntu o Fedora o


desde la pagina oficial instalandolo de forma similar a lo hecho en la práctica del
Wiimote.

La ayuda completa sobre Nmap puede consultarse en la página:


http://nmap.org/book/man.html o con el comando man nmap.

Nessus
Nessus es un software que sirve para explorar las vulnerabilidades de los equipos
conectados a una red computacional, es decir que busca bugs de seguridad. Consiste
de dos partes, un servidor nessusd, que es quien realiza la exploración de los
objetivos, y un cliente nessus, que muestra el reporte de los resultados de la
exploración. Nessus tiene una versión libre y una versión de paga, ésta última tiene la
ventaja de permitir el acceso a las vulnerabilidades recientemente descubiertas. Para
la detección de dispositivos puede usar algún escáner propio o externo, como nmap.

Nessus maneja las pruebas de vulnerabilidad en forma de plug-ins, mismos que son
obtenidos automáticamente por el servidor nessus del sitio principal de Nessus. Se
puede concluir que Nessus es una herramienta de evaluación de vulnerabilidades.

Entre las vulnerabilidades potenciales que permite detectar están:


 Las que permiten a un atacante el acceso remoto a datos sensibles o el control
de dispositivos.
 Mala configuración, incluyendo el uso de la configuración por defecto.
 Contraseñas por defecto, comunes o ausencia de ellos en algunas cuentas de
sistema.
 Denegación de servicio (DoS, Denial of Service) contra el stack TCP/IP.

Nessus funciona mediante navegación Web, en versiones anteriores a la 5 cualquier


host podía servir como cliente, al cambiar la licencia de hogar, sólo la maquina
servidor puede acceder vía web al mismo.
Figura 5 - Una auditoría completa es de múltiples puntos, Nessus solo pertenece a un
solo elemento.

Instalación de Nessus 5
Para la instalación primero es requerido la descarga del servidor a diferencia de la
práctica de cliente servidor donde utilizamos directamente los programas apt o yum
para acceder a los repositorios publicos de Ubuntu o Federoa en esta ocasión
descargaremos el archivo desde fuera, especificamente desde el sitio oficial de
Nessus (www.nessus.org ) .

Nota: Los siguientes son unos pasos breves, se recomienda ampliamente que se
sigan los tutoriales de instalación y configuración que ofrece el sitio oficial ya que están
completos y abarcan todas las posibles eventualidades que pueden surgir.

1. Ir al sitio de descarga.
2. Seleccionar nessus para el Sistema Operativo que se usara.
3. Descargar el manual de instalación. Liga PDF
4. Descargar el manual de usuario. LIGA PDF
5. Seguir las indicaciones de la guia de instalación (Ubuntu Pag. 13 a 15)
6. Seguir los pasos de arranque el Dameon ( Ubuntu Pag. 15 a 17)
7. Inicie el servidor de Nessus
8. Para los pasos de instalación de licencia y creación de usuarios siga lo indicado
a partir de la página 30 hasta la 41 (Todo los S.O.)
9. Para la instalación de licencia asegúrese de utilizar HomeFeed que es una
versión gratuita.
NOTA: Tanto la guía de instalación y la guía de usuario tienen incluido la
configuración y manejo de Nessus para todo los S.O. en los que da soporte,
por lo tanto deben de navegar a través del índice del inicio.

NOTA 2: Si aparece lo del sitio no seguro se debe que el proceso que sigue
SSL requiere de un tercero para validar pero es algo que no se hará para esta
práctica, configure su navegador para que acepte añadir una excepción a su servidor
Nessus. Mientras se estén siguiendo los pasos de la guía de instalación notará que
deberá registrar Nessus, se utilizará la versión casera que no tiene costo.

Una vez esté inicializado Nessus, siga los pasos del manual de usuario para su
ejecución. Recuerde que el puerto por defecto es 8834. El objetivo será generar una
auditoría a nuestros propios equipos o el de un compañero.

Figura 6 - Auditorías pre-existentes

Las políticas sirven como mecanismo de auditoría de seguridad, es necesario


entender que escanear un dispositivo no es una auditoría per-se, ya que
una auditoría debe llevar distintos pasos además de estar enfocada a lo que
queremos auditar, para que sea robusta y confiable.
Para esta práctica basta con utilizar "internal network scan" sobre nuestros dispositivos
para un análisis completo.

Figura 7 - Reporte de aplicar una auditoría a un dispositivo Android en la red del TEC

Si aparecen reportes de alto riesgo se le recomienda seguir los pasos indicados por
Nessus para eliminar la amenaza. Si por alguna razón aparecen varios malware o
incluso reportes de ser parte de una Botnet se recomienda una limpieza completa y
reinstalar desde cero el S.O.

Recuerde que solo un equipo completamente aislado o uno con un sistema de


seguridad muy robusto y extremadamente complejo (y posiblemente ineficiente)
regresaría un saldo blanco. Por lo general habrán elementos que inevitablemente
suelten algo de información.

Figura 8 - Para la práctica será requerido descargar el reporte en html

FAQ:
Error: No se encuentra el archivo Python.h
Solución: Instalar el paquete python-dev (ubuntu) ó python-devel (fedora)

Toda pregunta referente a Nessus, es preferible consultar directamente del sitio de


Nessus.
http://www.nessus.org/products/nessus/nessus-faq

==== Laboratorio ====


Formulario Vulnerabilidades

 Laboratorio: VULNERABILIDADES

Nmap.
(1) - Para instalar Nmap:
Abre una terminal.
Teclea:
Para Fedora:
yum install nmap
Para Ubuntu:
apt-get install nmap

Si deseas instalar la interfaz gráfica, deberás instalar también el paquete:


zenmap

Nota: puede ser más recomendable bajar nmap de su sitio, pues en


ocasiones el apt-get no baja la versión más reciente.

(2) - Realiza una exploración de puertos, con la opción -A al servidor javax:


http://javax.mty.itesm.mx/

(3) - Ahora, realiza una exploración de puertos a uno de tus compañeros,


como en el punto anterior. Reporta una captura de pantalla con los resultados
que obtuviste.

Nota: esta opción puede no funcionar bien con Ubuntu en máquina virtual. A
veces éstas no funcionan bien con acceso a dispositivos diferentes al disco
duro o USBs, o a puertos

Nessus
(4)- Instale, Nessus, para ello diríjase al sitio oficial para realizar su
descarga e instalación, la versiòn casera es gratuita, es importante que lo
descargue del sitio oficial para tener una versión funcional.

(5)- Ejecute el servidor Nessus y desde un cliente hágase escaneo a sí


mismo. Para cualquier duda, consulte el manual de usuario de Nessus.

Referencias:
Nmap Reference Guide, http://nmap.org/book/man.html
Anderson, Harry. Introduction to Nessus. Security Focus.
http://www.securityfocus.com/infocus/1741
http://www.nessus.org/nessus/
http://www.nessus.org/documentation/nessus_4.0_installation_guide.pdf

2012 Study on Applicaction Security: A survey of IT Security and Developers; E.


Adams, L. Ponemon; IEEEXplore

Tipos de ataque: Seguridad Informática Argentina; http://www.segu-


info.com.ar/ataques/ataques.htm

http://indigospv.com/noticias/?p=131

Mac OS X tuvo 58 variantes de malware en 9 meses; Seguridad Apple;


http://www.seguridadapple.com/2012/01/mac-os-x-tuvo-58-variantes-de-malware.html

egu-Info: Animación para comprender las Botnets http://blog.segu-


info.com.ar/2010/12/animacion-para-comprender-las-botnets.html#ixzz1qZlWBBCX

Laboratorio de Sistemas Operativos. Dr. Juan Arturo Nolazco. 2009.


[1]

Práctica 11: Benchmarks

Objetivo
El objetivo de esta sesión es integrar un poco los conocimientos adquiridos en las
prácticas de programación en shell mediante bash y en las de lenguaje C. Con este
laboratorio se espera acercar al alumno a una técnica básica de testing, el
benchmarking.
Introducción
Con la creciente cantidad de tecnología, aplicaciones y sobre todo datos, las
características que debe tener todo código son cada vez más requisitosas. Una de
estas características es el rendimiento (performance) que es la comparación de la
ejecución de un programa con ciertos indicadores de calidad, principalmente tiempo de
ejecución, uso de memoria, uso de disco, cantidad de operaciones de I/O entre otros.

Parte de nuestra labor como Ingenieros es ofrecer aplicaciones de software y/o


hardware que tengan el mejor rendimiento posible. Por esto, en muchas ocasiones es
necesario probar nuestras aplicaciones con cientos, miles o inclusive más variaciones
en variables o en pequeños pedazos de código, buscando obtener los mejores
resultados, o simplemente buscando documentar el comportamiento de las
aplicaciones en ambientes variados. A esta técnica se le conoce como benchmarking.

Hoy en día existen algunas herramientas que pueden realizar benchmarkings de


aplicaciones, sin embargo, es importante conocer formas de elaborarlo a la medida de
nuestras aplicaciones ya que es necesario que el benchmarking esté bien diseñado y
a la medida para que brinde información relevante acerca del código a analizar.

En este laboratorio practicaremos la técnica de benchmarking mediante el uso de


scripts, compiladores y comandos en la terminal. Para esto, practicaremos con dos
técnicas de variación de argumentos.

Método 1 Argumentos de ejecución


Primero cambiaremos las variables de ejecución pasándolas como argumentos a la
hora de ejecutar nuestros programas, como lo hemos hecho a lo largo del semestre,
mediante el uso de argv[]. Con este método, el código se compila una sola vez y
después es ejecutado utilizando distintas variables de entrada.

Ejemplo:

gcc mmult.c -o mmult //Compila el código


./mmult “argumento_1” //Ejecuta con primer set de variables
./mmult “argumento_2” //Ejecuta con segundo set de variables
./mmult “argumento_3” //Ejecuta con tercer set de variables
….

Nota: Como se puede ver, sería posible hacer el benchmarking “a mano” simpelmente
ejecutando el comando muchas veces, sin embargo cuando es necesario ejecutar
cientos o miles de instancias del programa variando pequeñas cosas, es bastante más
práctico el uso de scripts.

Método 2 Argumentos de compilación


El primer método es bastante efectivo y cien por ciento funcional. Sin embargo, ¿Qué
pasaría si quisiéramos variar partes del código al momento de compilar? Digamos
para, incluir o no ciertas librerías o pedazos de código, para variar posibilidades de
cómputo en paralelo, etcétera.

Para esto, el lenguaje C cuenta con directivas (o pragmas) que son partes del código
que son ejecutadas por el preprocesador al momento de compilar. Estas líneas
comienzan con el caracter #. Los #include 's al incio de nuestros códigos son un
ejemplo de directivas.

Otro ejemplo de directivas con el que trabajaremos en este laboratorio es #define. Esta
directiva crea un macro (una asociación de un identificador con una cadena de
caracteres). Haciendo uso de esta directiva es posible cambiar todas las instancias del
la cadena de caracteres del identificador en el código, por la cadena de caracteres
asignada en el #define al momento de compilación.

Ejemplo:

#define NUM=5

int main(){
for(NUM;NUM>0;NUM--){
//CODIGO
}
}
//En este caso, el CODIGO se ejecutaría 5 veces, ya que, al momento de compilación,
todas las instancias de NUM fueron sustituidas con el número 5.

Ahora, mezclando el uso de directivas con la herramienta de compilación que hemos


estado utilizando (gcc) es posible lograr un efecto similar al primer método, pero
modificando las variables al compilar el programa. Esto lo logramos mediante la opción
-D

Ejemplo:

MiPrograma.c
#define NUM

int main(){
for(NUM;NUM>0;NUM--){
//CODIGO
}
}

gcc MiPrograma.c -DNUM=3 -o MiPrograma


./MiPrograma

gcc MiPrograma.c -DNUM=4 -o MiPrograma


./MiPrograma

gcc MiPrograma.c -DNUM=5 -o MiPrograma


./MiPrograma
==== Laboratorio ====
Liga a la práctica de Laboratorio: Formulario

La práctica consiste en realizar un pequeño benchmark


sobre un código de multiplicación de matrices, variando su
magnitud y reportando los tiempos de ejecución.
A continuación se presenta un código que crea dos
matrices de 10x10, las llena de numeros aleatorios y
después las multiplica. Finalmente regresa el tiempo que
tardó en ejecutarse la multiplicación (Ojo. Por fines
prácticos se mide tiempo de usuario únicamente)
mmult.c
#include <stdio.h>
#include <stdlib.h>
#include <time.h>
int main(void)
{
static int A[10][10], B[10][10], C[10][10] = {0};
int i, j, k;
srand(time(NULL));
for(i = 0; i < 10; i++)
{
for(j = 0; j < 10; j++)
{
A[i][j] = rand()%100;
B[i][j] = rand()%100;
}
}
clock_t begin, end;
double time_spent;
begin = clock();
for(i = 0; i < 10; i++)
for(j = 0; j < 10; j++)
for(k = 0; k < 10; k++)
C[i][j] += A[i][k] * B[k][j];
end = clock();
time_spent = (double)(end - begin) /
CLOCKS_PER_SEC;
printf("Elapsed time: %.2lf seconds.\n", time_spent);
return 0;
}
Ejercicio 1.-Modifica el código mmult.c para que pueda
recibir el tamaño de las matrices como argumento desde la
terminal.
Ejercicio 2.- Escribe un script en bash (tiempo de
desempolvar las prácticas 1 y 2 del laboratorio) que
ejecute tu programa mmult modificado mediante un ciclo
que varíe el tamaño de las matrices en 4, 8 , 18, 32, 64,
128, 256, 512, 1024, 2048 y 4096 (Puedes aprovechar el
tiempo de ejecución viendo el uso de CPU de tu máquina
con la herramienta “System Monitor” incluida en ubuntu).
Ejercicio 3.- Modifica tu script y tu código para que la
salida, en lugar de la terminal, se dirija a un archivo
llamado 'salida.txt' en algún formato del cual sea fácil
copiar y pegar en un software graficador (Opcionalmente,
puedes copiar y pegar tu salida de este archivo en
LibreOffice Calc o Excel y genear una gráfica para que
puedas visualizar tus resultados)
Ejemplo
salida.txt
Tamaño Tiempo
10 .002 segundos
100 .2 segundos
1000 2.8 segundos


Ejercicio 4.-Modifica tu código mmult.c y tu script para


realizar el mismo ejercicio de benchmarking, pero
cambiando las variables al compilar y no al ejecutar el
programa.

COMPLEMETO 4

LABORATORIO DE SISTEMAS OPERATIVOS

Práctica 13: Seguridad informática: Vulnerabilidades,


Amenazas y Ataques.

I.Seguridad en informática.
i.Vulnerabilidades, Amenazas, Ataques y Controles.
ii.Errores de programa no mal intencionados.
i.Buffer Overflows
II. Vulnerabilidades en programas: Buffer Overflow
i.Estructura en memoria de un programa:
ii.Buffer overflow.
III. Referencias
IV.Laboratorio

Seguridad en informática.

Cualquier parte de un sistema computacional puede ser el objetivo de un crimen.


Un sistema computacional se puede ver como una colección de hardware, software,
medio de almacenamiento, datos y personas que una organización usa para
realizar una tarea de computo. A veces, asumimos que las partes de un sistema
computacional no tienen valor para un intruso, pero a menudo nos equivocamos.
Por ejemplo, tendemos a pensar que los bienes más valiosos en un banco son el
dinero, el oro y la plata en la bóveda. Pero de hecho la información del cliente
dentro de la computadora del banco puede ser más valiosa. Almacenada en papel,
grabada en un medio de almacenamiento, residente en memoria, o transmitida por
la líneas de teléfono o por enlaces satelitales, esta información puede ser usada en
miles de formas para ganar dinero de forma ilícita. Un banco de la competencia
podría usar esta información para robar clientes o incluso interrumpir el servicio y
desprestigiar al banco. Un individuo sin escrúpulos podría transferir dinero de una
cuenta a otra sin el permiso del propietario. Un grupo de estafadores podría
contactar una gran número de depositantes y convencerlos de invertir en planes
fraudulentos. La variedad de objetivos y ataques vuelve la seguridad informática
muy difícil.

Cualquier sistema es más vulnerable en su punto más débil. Un ladrón que intente
robar algo de tu casa no intentará penetrar una puerta metálica de cinco
centímetros de ancho si una ventana le da un acceso más fácil. Del mismo modo,
un sistema sofisticado de seguridad física perimetral no compensa el acceso sin
vigilancia a través de una simple línea telefónica y un MODEM. Esta idea es uno de
los principios de la seguridad computacional.

Principio de la penetración más fácil. Se debe esperar que un intruso utilice


cualquier medio de penetración disponible. La penetración no necesariamente
será por el medio más obvio, ni es necesariamente contra aquel que tiene la
defensa instalada más sólida. Y no tiene que ser en la forma en que esperamos que
se comporte el atacante.

Este principio implica que un especialista en seguridad computacional debe


considerar todos los medios de penetración posibles. Más aún, el análisis de
penetración debe ser hecho continuamente y especialmente cuando el sistema y su
seguridad cambien. Algunas veces, las personas subestiman la determinación y la
creatividad de los atacantes. Recuerda que la seguridad computacional es un juego
que solo tiene reglas para el equipo defensivo: los atacantes pueden (y lo harán)
usar cualquier medio que puedan. Tal vez lo más difícil para las personas fuera de
la comunidad de seguridad es pensar como el atacante. Un grupo creativo de
investigadores de seguridad, investigó un sistema de seguridad inalámbrica y
reportó una vulnerabilidad al jefe de diseño del sistema, quien respondió “que
funcionaría, pero ningún atacante lo intentaría”. No pienses eso ni por un minuto:
ningún ataque está fuera de límites.

Fortalecer un aspecto de un sistema podría, simplemente, volver otro medio de


penetración más atractivo para los intrusos. Por esta razón, se analizan las
distintas formas en que un sistema puede ser violado.

Vulnerabilidades, Amenazas, Ataques y Controles.

Un sistema basado en computadora tiene tres componentes distintos pero


valiosos: hardware, software y datos. Cada uno de estos activos ofrece valor a los
diferentes miembros de la comunidad afectada por el sistema. Para analizar la
seguridad, podemos realizar una lluvia de ideas sobre las formas en las que el
sistema o su información puede experimentar algún tipo de pérdida o daño. Por
ejemplo, podemos identificar datos cuyo formato o contenido debería ser
protegido de alguna forma. Queremos que nuestro sistema de seguridad asegure
que ningún dato es divulgado a partes no autorizadas. Tampoco queremos que los
datos sean modificados de formas ilegítimas. Al mismo tiempo, debemos asegurar
que los usuarios legítimos tienen acceso a los datos. De esta forma podemos
identificar debilidades en el sistema.

Una vulnerabilidad es una debilidad en el sistema de seguridad, por ejemplo, en


procedimientos, diseño o implementación, que podría ser explotada para causar
pérdida o daño. Por ejemplo, un sistema particular podría ser vulnerable a la
manipulación de datos no autorizada debido a que el sistema no verifica la
identidad de un usuario antes de permitirle el acceso a los datos.

Una amenaza a un sistema computacional es un conjunto de circunstancias que


tienen el potencial de causar una pérdida o daño. Para ver la diferencia entre una
amenaza y una vulnerabilidad vea la Figura 1; en ella se puede ver una pared
conteniendo el agua, el agua a la izquierda de la pared es una amenaza para el
hombre a la derecha de la pared; el agua podría aumentar de nivel, inundando al
hombre, o podría permanecer por debajo de la altura de la pared y causar que la
pared colapse. Así que la amenaza de daño es el potencial para el hombre de
mojarse, lastimarse o ser ahogado. Por ahora, la pared está intacta, por lo que la
amenaza para el hombre está no realizada.

Sin embargo, podemos ver una pequeña fractura en la pared, una vulnerabilidad
que amenaza la seguridad de la persona. Si el agua se eleva al nivel (o más allá) de
la fractura, explotará la vulnerabilidad y dañará a la persona.

Hay muchas amenazas para un sistema computacional, incluyendo aquellas


iniciadas por humanos o iniciadas por computadora. Todos hemos experimentado
los resultados de errores humanos inadvertidos, defectos de diseño de hardware, y
fallas de software. Pero también los desastres naturales son amenazas; estos
pueden detener un sistema cuando el cuarto de la computadora se inunda o
cuando el data center se colapsa por un terremoto.

Un humano que explota (o aprovecha) una vulnerabilidad perpetra un ataque al


sistema. Un ataque también puede ser iniciado por otro sistema, como cuando un
sistema envía a otro un abrumador conjunto de mensajes, virtualmente deteniendo
la habilidad de funcionar del segundo . Desafortunadamente se ha visto este tipo
de ataque frecuentemente, se conoce como denegación de servicio (DoS) y consiste
en inundar un servidor con mas mensajes de los que puede manejar.

Figura 3 - En la práctica es casi imposible evitar toda las vulnerabilidades en un


sistema, pero las amenazas pueden ser controlables para garantizar un riesgo bajo o casi
nulo.

¿Cómo podemos abordar estos problemas? Usamos un control como una medida
de protección. Esto es, un control es una acción, un dispositivo, un procedimiento o
una técnica que remueve o reduce una vulnerabilidad. En la Figura 1, la persona
puede poner un dedo en un hoyo y así controlar la amenaza de que el agua se
fugue, hasta que encuentre una solución al problema más permanente. En general,
podemos describir la relación entre amenazas, controles y vulnerabilidades de esta
forma: Una amenaza se bloquea por un control de una vulnerabilidad.

Los daños ocurren cuando una amenaza se lleva a cabo contra una vulnerabilidad.
Para proteger contra el daño, entonces, podemos neutralizar la amenaza, cerrar la
vulnerabilidad, o ambos. La posibilidad de que un daño ocurra se conoce como
riesgo. Podemos hacer frente a los daños de diferentes maneras. Podemos tratar
de:
⇒ prevenir, bloqueando el ataque o cerrando la
vulnerabilidad,
⇒ disuadir, haciendo el ataque más difícil pero no imposible,
⇒ desviar, volviendo otro objetivo más atractivo, o este no
tanto,
⇒ detectar, cuando sucede o algún tiempo después,
⇒ recuperar de sus efectos.

Errores de programa no mal intencionados.

Los seres humanos, programadores y otros desarrolladores comenten muchos


errores, la mayoría de los cuales no son intencionales ni maliciosos. Muchos de
estos errores causan el mal funcionamiento de un programa, pero no dan lugar a
más vulnerabilidades de seguridad graves. Sin embargo, algunas clases de errores
han atormentado a los programadores y profesionales de seguridad por décadas, y
no hay razón para creer que desaparecerán. El buffer overflow (desbordamiento
de búfer) es uno de ellos.

Buffer Overflows

Un buffer overflow es el equivalente computacional de tratar de vaciar dos litros


de agua en una jarra de un litro, algo de agua se derramará y hará un desorden. Y
cuántos desórdenes han hecho en computación estos errores.

Todos los elementos de un programa y los datos están en memoria durante la


ejecución, compartiendo espacio con el sistema operativo, otro código y rutinas
residentes. Hay cuatro casos a considerar:
⇒ si los datos se desbordan en el espacio de datos del usuario,
simplemente sobrescriben un valor de variable existente, o podrían ser
escritos en una ubicación ya usada, quizás afectando el resultado del
programa pero no otros programas o datos;
⇒ los datos desbordados entran en el área de programa del
usuario, si sobrescriben instrucciones ya ejecutadas (que no serán
ejecutadas de nuevo) el usuario no percibirá algún efecto, de lo
contrario si sobrescribe una instrucción que no se ha ejecutado aún, la
máquina intentará ejecutar las instrucciones que correspondan a los
códigos de operación; solo el usuario experimentará un efecto;
⇒ los otros dos casos, más interesantes, ocurren cuando el
sistema posee el espacio inmediatamente después del búfer que se
desborda; el desborde sobre datos del sistema o áreas de código
producen resultados similares al caso anterior: cálculos con valores
defectuosos o ejecución de operaciones inadecuadas.

Vulnerabilidades en programas: Buffer Overflow


Estructura en memoria de un programa:

Históricamente un programa escrito en C se compone las siguientes segmentos en


memoria.

Texto: Las instrucciones en código maquinal que debe ejecutar el


procesador. En este segmento sólo se permiten operaciones de lectura para
evitar que se modifiquen las instrucciones accidentalmente.
Datos: Almacena las variables globales y estáticas inicializadas en el
programa.
Datos no inicializados (bss): Al igual que el segmento de datos, contiene el
espacio de memoria necesario para las variables globales y estáticas que no
fueron inicializadas por el programador. El kernel las inicializa con un valor
de 0 o NULL antes de comenzar la ejecución del programa.
Pila (stack): Es donde las variables automáticas, aquellas locales a
funciones, son almacenadas junto con información que es guardada cada
vez que una función es llamada. Al hacer una llamada a función, la dirección
de regreso así como los argumentos de la función son almacenados en la
pila. En la arquitectura x86 este segmento comienza en la parte alta de
memoria y crece hacia la parte baja.
Heap: Es el espacio que utiliza el kernel para almacenar la memoria que le
es solicitada con la familia de funciones malloc(). Este segmento utiliza "el
mismo espacio" que el stack con la diferencia que el heap comienza en la
parte baja

de memoria y crece hacia la parte alta, en sentido contrario al stack.


Imagen - En este esquema se puede apreciar la forma en que qeuda un programa tipico en
C/C++ en memoria

Buffer overflow.

Desde el inicio de las computadoras han existido las vulnerabilidades relacionadas


con el "buffer overflow" y aún en estos tiempos es uno de los problemas de
seguridad más comunes. Aunque C es un lenguaje de alto nivel, el sistema espera
que el programador mantenga cierta responsabilidad sobre la integridad de los
datos que maneja. Esto es debido a que si esta responsabilidad se pasara al
compilador o al tiempo de ejecución tendría una repercusión en rendimiento
bastante alta.

Así como la simpleza de C aumenta, el control del programador y la eficiencia del


programa final, lo que también resulta en programas que son vulnerables a "buffer
overflow" o a pérdidas (fugas) de memoria (memory leaks) si no se tiene cuidado.
Esto significa que una vez que una variable es memoria asignada, no hay forma
nativa de asegurar que el dato es del tamaño adecuado a la localidad de memoria
donde se desea escribir. Si el programador quiere almacenar 10 bytes de datos en
un buffer con 8 bytes de tamaño, podrá hacerlo ya que es una operación permitida,
incluso si eso puede causar una falla de segmento. Esta operación es llamada
"buffer overflow" porque se escribieron 2 bytes en el buffer fuera del espacio que
se tenía reservado para él reescribiendo los datos que se encuentren ahí.

Este tipo de vulnerabilidades pueden utilizarse para diferentes propósitos como lo


son cambiar el valor de una variable que sirva como autenticación o cambiar el
flujo de ejecución de un programa. Este último es el exploit (explotación de la
vulnerabilidad) que los hackers buscan con más frecuencia. Para lograr esto se
busca cambiar el valor de retorno de la función que se está ejecutando para que el
IPX tome la dirección de un buffer que contenga cierto código que deseemos que se
ejecute.

A continuación se verá un ejemplo de cómo se puede obtener acceso a un sistema


vulnerable a este tipo de exploits. El siguiente programa, auth_overflow.c, es una
simplificación del proceso de autenticación, simplemente tiene un grupo de claves
que son permitidas y a cualquier otra clave, se le negará el acceso.

Código auth_overflow.c:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int check_authentication(char *password) {


int auth_flag = 0;
char password_buffer[16];

strcpy(password_buffer, password);

if(strcmp(password_buffer, "brillig") == 0)
auth_flag = 1;
if(strcmp(password_buffer, "outgrabe") == 0)
auth_flag = 1;

return auth_flag;
}

int main(int argc, char *argv[]) {


if(argc < 2) {
printf("Usage: %s <password>\n", argv[0]);
exit(0);
}
if(check_authentication(argv[1])) {
printf("\n-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
printf(" Access Granted.\n");
printf("-=-=-=-=-=-=-=-=-=-=-=-=-=-\n");
} else {
printf("\nAccess Denied.\n");
}
}
La ejecución del programa:

[jnolazco@localhost pract15]$ ./auth_overflow brillig

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
[jnolazco@localhost pract15]$
[jnolazco@localhost pract15]$ ./auth_overflow outgrabe

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-
[jnolazco@localhost pract15]$ ./auth_overflow test

Access Denied.
[jnolazco@localhost pract15]$

Se puede observar cómo la variable que indica si se aceptó o no la clave está en el


mismo segmento de memoria que el buffer donde se almacena la contraseña
proporcionada por el usuario. Se puede almacenar esto utilizando el depurador de
GNU, gdb. Para esto se compilará el programa con la bandera -g de gcc, que agrega
la información de depuración en el binario ejecutable, con el comando:

[jnolazco@localhost pract15]$ gcc -g -


o auth_overflow auth_overflow.c

y se correrá el programa del siguiente modo:

[jnolazco@localhost pract15]$ gdb -q ./auth_overflow


Reading symbols from /home/jnolazco/pract15/auth_overflow...done.
(gdb) list 1
1 #include <stdio.h>
2 #include <stdlib.h>
3 #include <string.h>
4
5 int check_authentication(char *password) {
6 int auth_flag = 0;
7 char password_buffer[16];
8
9 strcpy(password_buffer, password);
10
(gdb)
11 if(strcmp(password_buffer, "brillig") == 0)
12 auth_flag = 1;
13 if(strcmp(password_buffer, "outgrabe") == 0)
14 auth_flag = 1;
15
16 return auth_flag;
17 }
18
19 int main(int argc, char *argv[]) {
20 if(argc < 2) {
(gdb) break
No default breakpoint address now.
(gdb) break 9
Breakpoint 1 at 0x80484a1: file auth_overflow.c, line 9.
(gdb) break 16
Breakpoint 2 at 0x80484ef: file auth_overflow.c, line 16.
(gdb)

Se establecen dos puntos en los que queremos que la ejecución del programa se
detenga con el comando break.

(gdb) run AAAAAAAAAAAAAAAAAA


Starting program: /home/jnolazco/pract15/auth_overflow
AAAAAAAAAAAAAAAAAA

Breakpoint 1, check_authentication (password=0xbffff940 'A' <repeats


18 times>) at auth_overflow.c:9
9 strcpy(password_buffer, password);
(gdb) x/s password_buffer
0xbffff68c: "T\203\004\b\340\002\377\267\020\230\004\b\310\366\
377\277"
(gdb) x/x &auth_flag
0xbffff69c: 0x00000000
(gdb) print 0xbffff69c - 0xbffff68c
$1 = 16
(gdb) x/16xw password_buffer
0xbffff68c: 0x08048354 0xb7ff02e0 0x08049810 0
xbffff6c8
0xbffff69c: 0x00000000 0xb7fc7304 0xb7fc6ff4 0
xbffff6c8
0xbffff6ac: 0x08048535 0xbffff940 0xb7ff02e0 0
x0804858b
0xbffff6bc: 0xb7fc6ff4 0x08048580 0x00000000 0
xbffff748
(gdb)

Deteniendo la ejecución del programa justo antes de la función strcpy().


Examinando el valor contenido en password_buffer se puede observar que tiene
datos aleatorios así como auth_flag que tiene un valor de 0x00000000. También se
puede observar la dirección de memoria en la que se encuentran dichos valores. Es
importante notar que la dirección de auth_flag está 16 bytes adelante de
password_buffer, esto quiere decir que si logramos escribir algo más grande que 16
bytes en este buffer podríamos reescribir el valor de esta variable sensible.

(gdb) continue
Continuing.

Breakpoint 2, check_authentication (password=0xbffff94c 'A' <repeats


18 times>) at auth_overflow.c:16
16 return auth_flag;
(gdb) x/s password_buffer
0xbffff69c: 'A' <repeats 18 times>
(gdb) x/x &auth_flag
0xbffff6ac: 0x00004141
(gdb) x/16xw password_buffer
0xbffff69c: 0x41414141 0x41414141 0x41414141 0
x41414141
0xbffff6ac: 0x00004141 0xb7fc7304 0xb7fc6ff4 0
xbffff6d8
0xbffff6bc: 0x08048535 0xbffff94c 0xb7ff02e0 0
x0804858b
0xbffff6cc: 0xb7fc6ff4 0x08048580 0x00000000 0
xbffff758
(gdb) x/4cb &auth_flag
0xbffff6ac: 65 'A' 65 'A' 0 '\000' 0 '\000'
(gdb) x/dw &auth_flag
0xbffff6ac: 16705
(gdb) continue
Continuing.

-=-=-=-=-=-=-=-=-=-=-=-=-=-
Access Granted.
-=-=-=-=-=-=-=-=-=-=-=-=-=-

Program exited with code 034.


(gdb)

Dejando continuar el programa después de la función strcpy() se vuelven a


analizar las mismas localidades de memoria y se puede observar que la memoria
en efecto fue modificada con el dato que se dio como entrada a la función. Además
se ve cómo el password_buffer se sobrepasó hasta auth_flag cambiando los
primeros dos bytes de esa variable. Si se sigue la ejecución del código se puede ver
que para aceptar la autenticación se utiliza una comparación contra cero. Habiendo
escrito cualquier cosa en la variable de autenticación se hace diferente de cero, por
lo tanto se tiene acceso al sistema.

Se puede ver claramente que es posible utilizar esta vulnerabilidad a nuestro favor
por dos fallas en el código. Primero, no se verifica el tamaño del buffer que se va a
copiar permitiendo que el usuario pueda proporcionar un buffer más grande del
que el programador había considerado sobre-escribiendo así los valores de las
siguientes localidades de memoria. El segundo error de código es que una variable
tan sensible está más adelante en memoria que un buffer vulnerable.

Referencias
Erickson, Jon. Hacking: The Art Of Exploitation. 2° Ed.
Stevens, Richard; Rago, Stephen. Advanced Programming in the Unix Environment.
2°Ed.
Pfleeger, Charles. Security in Computing. 4th Ed. Prentice Hall. 2006.

Laboratorio
Laboratorio Dr Nolazco
Laboratorio Dr Icaza
Laboratorio de Sistemas Operativos. Dr. Juan Arturo Nolazco. 2009.
[1]

COMPLEMETO 8 PDF

También podría gustarte