Lenguaje ensamblador para programadores en lenguajes de alto nivel: condicionales / Sudo Null IT News
EN
artículo anterior
Aprendimos los conceptos básicos de la sintaxis del lenguaje ensamblador y pudimos crear un programa usando solo dos comandos. ¡Resultado impresionante!
En esta lección, aprenderemos nuevos comandos y usaremos ese conocimiento para llevar nuestra primera construcción de alto nivel al lenguaje ensamblador: los condicionales.
▍ Comandos de transferencia de control
La secuencia de comandos decodificados y ejecutados por la CPU se llama
flujo de comandos
. Puedes representarlo como una matriz cuyos índices son
DIRECCIÓN
equipos
.
La instrucción que se está ejecutando actualmente es aquella cuya dirección está almacenada como un valor en un registro. rip
; por eso se llama registro índice de comando.
En pseudocódigo, la ejecución del programa debería verse así:
while (!exited) {
// Получаем команду в `registers.rip`
instruction = instruction_stream(registers.rip)
// Исполняем команду и возвращаем
// адрес следующей команды.
next_pointer = instruction.execute()
// Присваиваем `rip` значение нового адреса,
// чтобы на следующей итерации получить новую команду.
registers.rip = next_pointer
// Обрабатываем побочные эффекты (это мы рассматривать не будем)
}
La mayoría de las veces, la ejecución se produce de forma lineal: los comandos se ejecutan uno tras otro en el orden en que están codificados, de arriba a abajo. Sin embargo, algunos comandos pueden romper este orden; ellos son llamados
comandos de transferencia de control
(Instrucción de Transferencia de Control, CTI).
Los CTI que nos interesan pertenecen a la categoría condicional y incondicional; Proporcionan un flujo de control en lenguaje ensamblador al permitir la ejecución desordenada de instrucciones. Otro tipo de CTI son las interrupciones de software; no los consideraremos explícitamente2porque están estrechamente relacionados con los sistemas operativos y están fuera del alcance de nuestra serie de artículos.
El primer CTI que examinaremos será jmp
(salto, transición).
▍ Saltos incondicionales
Las transiciones le permiten ejecutar código en cualquier punto del flujo de comandos. Solo hacen falta cambiar
rip
después de lo cual, en el siguiente ciclo de reloj, la CPU tomará el comando en la nueva dirección.
Sintácticamente, la transición se ve así:
jmp label
Donde el operando denota la instrucción de destino.
Casi siempre el comando objetivo se indica mediante una etiqueta; En lenguaje natural, el comando anterior se puede describir de la siguiente manera: “Continuar la ejecución desde el comando cuya etiqueta label
».
El ensamblador, es decir, el software que convierte un programa en lenguaje ensamblador en código de máquina, convierte las etiquetas en direcciones numéricas del flujo de instrucciones, y al ejecutarse, esta dirección será asignada a un registro. rip
.
De hecho, las direcciones numéricas y los desplazamientos relativos también son válidos para rip
valores, pero son más convenientes para trabajar con las máquinas que para las personas. Por ejemplo, los compiladores con indicadores de optimización o desensambladores prefieren el direccionamiento numérico en lugar de etiquetas.
Los lectores atentos habrán notado que la transición que describimos no depende de ninguna condición: si el programa llega a esta línea, realizará la transición. Por lo tanto, dicho equipo se considera incondicional.
Veamos un ejemplo.
Usamos el mismo programa “hola mundo” de la primera lección. Hagámoslo más legible agregando transiciones para dividir el código en fragmentos. Al mismo tiempo, agregaremos constantes numéricas para eliminar los números mágicos de nuestro código.
section .data
; Так же, как и раньше, мы определяем константу msg
msg db `Hello, World!\n`
; На этот раз мы также определим здесь её длину,
; а также другие константы для повышения читаемости.
; Директива `equ` (equals) используется для определения
; числовых констант.
len equ 14 ; длина буфера
sys_write equ 1 ; идентификатор системного вызова write
sys_exit equ 60 ; идентификатор системного вызова exit
stdout equ 1 ; дескриптор файла для stdout
section .text
global _start
_start:
; Переходы могут показаться непонятными. Чтобы упростить это
; введение, мы используем пункты (1), (2) ...
; для описания этапов кода и их порядка.
; (1) Здесь мы мгновенно переходим к коду,
; выводящему сообщение. Конечная точка - это метка
; `print_msg`, то есть исполнение будет продолжено
; со строки прямо под ней.
; Давайте перейдём к (2), чтобы посмотреть,
; как разворачивается эта история.
jmp print_msg
exit:
; (3) Мы уже знаем принцип: при помощи `sys_exit` из
; верхнего блока мы можем вызывать системный вызов exit,
; чтобы выйти из программы с кодом состояния 0.
mov rax, sys_exit
mov rdi, 0
syscall
print_msg:
; (2) После вызова `jmp`, мы выполняем ту же
; подпрограмму, которую определили в первом уроке.
; Можете вернуться назад, если не помните точно,
; для чего нужны представленные ниже регистры.
mov rax, sys_write
mov rdi, stdout
mov rsi, msg
mov rdx, len
syscall
; Мы закончили с выводом, пока выполнять выход
; из программы. Снова используем переход для выполнения
; блока по метке `exit`.
;
; Стоит отметить, что если бы мы не перешли куда-то ещё,
; даже если больше кода для исполнения не осталось,
; программа не выполнила бы выход! Она осталась бы в чистилище
; и рано или поздно сгенерировала бы ошибку сегментации.
; Закомментируйте следующую строку, если захотите это проверить.
;
; Готово? Увидели, как поломалась программа? Отлично!
; А теперь исправим это, выполнив переход к метке `exit`.
; Отправляйтесь к (3), чтобы увидеть конец этой короткой истории о переходах.
jmp exit
▍ Saltos condicionales
Como habrás adivinado, implementamos el flujo de control condicional usando
condicional
CTI, y en particular
saltos condicionales
. No te preocupes, ya hemos sentado las bases, los saltos condicionales son sólo una extensión del mismo concepto de salto.
Al trabajar con lenguajes de alto nivel, es posible que esté acostumbrado a declaraciones condicionales flexibles como if
, unless
y when
. El lenguaje ensamblador adopta un enfoque diferente. En lugar de varias declaraciones condicionales genéricas, tiene una gran cantidad de comandos especializados para pruebas específicas.
Afortunadamente, estos comandos tienen una estructura de nombres lógica, lo que los hace fáciles de recordar.
Veamos un ejemplo:
jne label
Aquí
label
indica un comando en nuestro código, como es el caso de las ramas incondicionales. En lenguaje natural esto se puede leer como “
j
árbitro (ir) a
label
Si
norte
ot (no)
mi
cual (igual)”.
La siguiente tabla muestra los símbolos de salto condicional más comunes.3:
Aquí hay algunos ejemplos más:
je label
: “ir si es igual”jge label
: “saltar si es mayor o igual que”jnz label
: “saltar si no es nulo”.
Estos comandos hacen exactamente lo que dicen sus nombres: si la condición es verdadera, el programa salta a la etiqueta de destino. Si no, simplemente continúa en la siguiente línea. Al igual que con los saltos incondicionales, la ubicación del salto se puede especificar numéricamente.
Quizás se esté preguntando: “¿igual a qué?”, “¿mayor que qué?”, “¿cero comparado con qué?”
Respondamos a estas preguntas profundizando en la mecánica de las comparaciones en lenguaje ensamblador e introduciendo un registro especial que juega un papel fundamental en este proceso: el registro. eflags
.
▍ Banderas
eflags
es un registro de 32 bits que almacena varias banderas. A diferencia de los registros de propósito general,
eflags
se lee bit a bit y cada posición representa una bandera específica. Puede pensar en estos indicadores como un conjunto de valores booleanos integrados directamente en la CPU. Cuando un bit es 1, la bandera correspondiente tiene un valor
true
y cuando es 0, entonces la bandera es igual a
false
.
Las banderas están diseñadas para muchos propósitos diferentes.
pero lo único que nos importa es que se utilicen para proporcionar contexto después de la operación. Por ejemplo, si el resultado de una suma es cero, entonces el indicador de desbordamiento (OF) puede decirnos si en realidad fue causado por un cero o un desbordamiento. Son importantes para nosotros porque es con la ayuda de banderas que el lenguaje ensamblador almacena los resultados de las comparaciones.
En esta sección solo veremos las siguientes banderas:
- bandera cero (ZF), es igual a 1 cuando el resultado de la operación es cero,
- bandera de signo (SF), es igual a 1 cuando el resultado de la operación es negativo.
Equipo
cmp
(comparar, comparar) es una de las formas estándar de realizar comparaciones:
cmp rax, rbx
Este comando resta el segundo operando del primero sin almacenar el resultado, especificando banderas en su lugar. Por ejemplo:
- Si los operandos son iguales, la bandera cero (ZF) toma el valor 1.
- Si el primer operando es mayor que el segundo, entonces el indicador de signo (SF) toma el valor 0.
Habiendo aprendido sobre esto, comenzaremos a comprender el significado de los saltos condicionales:
- “ir si es igual” (
je
) significa “ir si ZF=1”, - “ir si es mayor o igual que” (
jge
) significa “saltar si SF=0 o ZF=1”, - “ir si no es nulo” (
jnz
) significa “ir si ZF=0”.5
▍ Finalmente condicionales
Finalmente estamos listos para escribir condicionales en lenguaje ensamblador. ¡Hurra!
Considere el siguiente pseudocódigo:
if rax == rbx
success()
else
error()
En lenguaje ensamblador podemos expresar esta lógica de la siguiente manera:
; Сравнить значения в rax и rbx
cmp rax rbx
; Если они равны, перейти к `success`
je success
; Иначе перейти к `error`
jmp error
Este código ensamblador primero compara los valores en los registros rax y rbx usando la instrucción cmp. Luego usa salto condicional e incondicional (
je
y
jmp
) para controlar el flujo de ejecución del programa en función del resultado de la comparación.
Veamos otro ejemplo, suficiente “hola mundo” para nosotros. Esta vez crearemos un software serio que realiza sumas y verifica si el resultado es igual al esperado. Muy grave.
section .data
; Первым делом мы задаём константы,
; чтобы повысить читаемость
sys_exit equ 60
sys_write equ 1
stdout equ 1
; Здесь мы задаём параметры нашей программы.
; Мы суммируем `a` и `b` и ожидаем, что результат
; будет равен значению константы `expected`.
a equ 100
b equ 50
expected equ 150
; Если сумма верна, мы хотим показать
; пользователю сообщение
msg db `Correct!\n`
msg_len equ 9
section .text
global _start
_start:
; Мы используем команду `add`, суммирующую
; два целых значения. `add` получает в качестве операндов
; регистры, поэтому мы копируем константы
; в регистры `rax` и `rbx`
mov rax, a
mov rbx, b
; Вот наша новая команда!
; Она использует арифметические способности
; CPU, чтобы суммировать операнды, и сохраняет
; результат в `rax`.
; На языках высокого уровня это выглядело бы так:
; rax = rax + rbx
add rax, rbx
; Здесь мы используем команду `cmp` (compare),
; чтобы проверить равенство rax == expected
cmp rax, expected
; `je` означает "перейти, если равно", так что если сумма
; (в `rax`) равна `expected` (константе), мы переходим
; к метке `correct`
je correct
; Если же результат неправильный, мы переходим
; к метке `exit_1`, чтобы выйти с кодом состояния 1
jmp exit_1
exit_1:
; Здесь то же самое, что и в предыдущем уроке,
; но теперь мы используем код состояния 1,
; традиционно применяемый, чтобы сигнализировать об ошибках.
mov rax, sys_exit
mov rdi, 1
syscall
correct:
; Мы уже знакомы с этим блоком: здесь мы
; делаем системный вызов `write` для вывода сообщения,
; говорящего пользователю, что сумма верна.
mov rax, sys_write
mov rdi, stdout
mov rsi, msg
mov rdx, msg_len
syscall
; После вывода сообщения мы можем перейти к
; `exit_0`, где выполняется выход с кодом
; состояния 0, обозначающим успех
jmp exit_0
exit_0:
; Это тот же самый код, который мы видели во всех
; предыдущих упражнениях; вам он должен быть знаком.
mov rax, sys_exit
mov rdi, 0
syscall
▍ Conclusión
Hasta ahora hemos dominado los componentes fundamentales del flujo de control en lenguaje ensamblador.
Aprendimos sobre las instrucciones de transferencia de control (CTI) a través del ejemplo de saltos condicionales e incondicionales. Hemos analizado cómo el puntero de comando (rip
) controla la ejecución del programa y cómo las transiciones manipulan ese flujo. Hemos estudiado el registro. eflags
y aprendió sobre su importante papel en las comparaciones al comprender cómo se relacionan la bandera cero (ZF) y la bandera de signo (SF) con los operadores condicionales. Conectando el equipo cmp
Con las transiciones, hemos creado un equivalente en lenguaje ensamblador de los condicionales de los lenguajes de alto nivel.
Las transiciones le permiten implementar el flujo de control más simple, pero complican la comprensión del código. En el próximo artículo, veremos el equivalente de funciones: una forma de ejecutar código desde otro lugar mientras se mantiene un flujo lineal. Verás que este enfoque es similar al código de procedimiento en lenguajes de alto nivel y que hace que el código ensamblador sea más claro y organizado.
1. Semejante abstracción no está completamente divorciada de la realidad. Los emuladores, es decir, el software que emula sistemas en otro hardware, suelen representar flujos de comandos como matrices. Si está interesado en la simulación, debería intentar comenzar con
y como guía introductoria utilice
.
2. Yo digo explícitamenteporque, por ejemplo, el comando syscall
puede causar una interrupción. La interacción entre los sistemas operativos y los programas de usuario es un mundo maravilloso en sí mismo, demasiado vasto para explorarlo en nuestros artículos. Si tienes curiosidad, lee cualquier libro sobre sistemas operativos. personalmente lo recomiendo OSTEP y en particular este capitulo.
3. Para una revisión completa, consulte “Saltar si se cumple la condición”. Manuales para desarrolladores de software Intel (SDM).
4. La lista completa se puede encontrar en la sección “Registro EFLAGS” Manuales para desarrolladores de software Intel (SDM).
5. Tenga en cuenta que las pruebas de igualdad y las pruebas de nulo son esencialmente lo mismo.
Canal de Telegram con descuentos, sorteos y novedades informáticas