Atlasingeniería

Arquitectura y sistemas operativosRepresentación y lógica digitalTema 1

Binario, hexadecimal y complemento a dos

Con dos símbolos alcanza para representar cualquier número, y con un truco de rotación alcanza para representar los negativos sin que el circuito de suma tenga que enterarse.

Todo lo que hace una computadora termina siendo una secuencia de bits. Lo interesante no es que los números se escriban en base dos —eso es mecánico— sino cómo se representan los negativos, que es una decisión de diseño con consecuencias que se ven en cualquier lenguaje.

Qué es una base, en realidad

Una base es sólo cuántos símbolos hay antes de necesitar otra posición. En binario, cada posición vale una potencia de dos, y un número de nn bits representa valores de 00 a 2n12^n-1.

El hexadecimal existe por comodidad humana: cada dígito hexadecimal equivale exactamente a cuatro bits, así que convertir es agrupar de a cuatro, sin cuentas. Por eso los volcados de memoria, los colores y las direcciones se escriben en hexa: es binario legible.

Antes de seguir, predecí

Un entero de 32 bits con signo vale 2.147.483.647 y le sumás uno. ¿Qué da?

Las tres formas de escribir un negativo

Para los negativos hubo varias opciones. Signo y magnitud reserva un bit para el signo: fácil de leer y con dos ceros distintos, el positivo y el negativo. Complemento a uno invierte todos los bits: también tiene dos ceros.

Los dos ceros no son una curiosidad estética: obligan al circuito a casos especiales en cada comparación. La opción que ganó, el complemento a dos, es invertir los bits y sumar uno, y no tiene ese problema.

El 5 en cuatro bits: 0101. El bit más a la izquierda en cero dice que es positivo.

1 / 6
El paso final es el que convence: sumar un número y su complemento a dos da todos ceros, con un acarreo que se cae del borde. Por eso restar es sumar el complemento, y por eso el circuito de la resta no existe: es el mismo sumador.

Por qué ganó complemento a dos

La ventaja del complemento a dos es que la resta desaparece como operación. Sumar un número negativo representado así da el resultado correcto usando el mismo sumador que para los positivos, descartando el acarreo que se sale.

Es aritmética modular: los valores se comportan como un anillo donde pasar del máximo vuelve al mínimo. El hardware no distingue con signo de sin signo al sumar; la diferencia aparece recién al comparar y al desplazar a la derecha. Un solo circuito para las dos cosas: por eso ganó.

Hay un negativo más que positivos

El rango de nn bits en complemento a dos va de 2n1-2^{n-1} a 2n112^{n-1}-1: hay un negativo más que positivos, porque el cero ocupa un lugar del lado positivo.

Eso produce una rareza real: el mínimo no tiene opuesto representable. Negar el entero más chico de 32 bits devuelve el mismo número, y el valor absoluto de ese valor es negativo. Es la fuente de una familia entera de bugs y de vulnerabilidades, y no es un caso hipotético.

El desbordamiento y cómo se detecta

El desbordamiento ocurre cuando el resultado no entra en los bits disponibles. En complemento a dos se detecta con una regla simple: sumar dos positivos y obtener un negativo, o dos negativos y obtener un positivo.

Qué pasa después depende del lenguaje. En C con enteros con signo es comportamiento indefinido, y el compilador optimiza asumiendo que no ocurre. En Java y Go se envuelve silenciosamente. En Rust aborta en desarrollo y envuelve en producción. En Python no pasa nada porque los enteros crecen sin límite. La misma operación, cuatro resultados distintos.

Dos costumbres que salen de esto

De acá salen dos costumbres útiles. Primera: el chequeo de desbordamiento se hace antes de la operación, no después, porque después el valor ya se perdió.

Segunda: el corrimiento a la derecha tiene dos variantes. La aritmética conserva el signo copiando el bit más alto; la lógica rellena con ceros. Usar la equivocada sobre un número negativo da un resultado absurdo, y es el motivo por el que dividir por dos con un corrimiento no es equivalente a dividir por dos para los negativos.

Lo que se ve desde un lenguaje

// Desplazar es multiplicar o dividir por potencias de dos
5 << 1;            // 10
5 >> 1;            // 2, y trunca hacia abajo

// El desbordamiento silencioso, en enteros de 32 bits
const max = 2 ** 31 - 1;   // 2147483647
(max | 0) + 1 | 0;         // -2147483648: dio la vuelta

// Las máscaras: banderas empaquetadas en un entero
const LEER = 0b100, ESCRIBIR = 0b010, EJECUTAR = 0b001;
const permisos = LEER | ESCRIBIR;          // 0b110
(permisos & ESCRIBIR) !== 0;               // true: ¿tiene el permiso?
const sinEscritura = permisos & ~ESCRIBIR; // 0b100: apagar un bit

Las tres partes son la misma representación vista desde arriba. El desplazamiento funciona porque cada posición vale una potencia de dos; el desbordamiento ocurre porque después del mayor positivo viene el patrón de bits que en complemento a dos significa el menor negativo; y las máscaras existen porque un entero es un conjunto de bits independientes.

Por qué complemento a dos ganó

Signo y magnitudComplemento a unoComplemento a dos
Representación del cerodos: +0 y −0dosuna sola
Sumar negativoscircuito apartecon acarreo de vueltael mismo sumador
Compararcasos especialescasos especialesdirecto
Rango con n bits−(2ⁿ⁻¹−1) a 2ⁿ⁻¹−1lo mismo−2ⁿ⁻¹ a 2ⁿ⁻¹−1
La segunda fila es toda la razón: con complemento a dos, restar es sumar el complemento, así que no hace falta un circuito de resta. Menos silicio y menos casos especiales.

Cierre

Binario para la máquina, hexadecimal para leerlo. El complemento a dos convierte la resta en suma y elimina el cero doble, a cambio de un rango asimétrico donde el mínimo no tiene opuesto. El desbordamiento se detecta por el cambio de signo y cada lenguaje decide distinto qué hacer con él.

Autoevaluación

¿Lo entendiste?

¿Por qué ganó el complemento a dos frente a signo y magnitud?
¿Por qué las direcciones de memoria y los colores se escriben en hexadecimal?
Con n bits en complemento a dos, ¿qué rango se representa?
¿Qué es un desbordamiento en complemento a dos?