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

1

.

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.

4

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:

▍ 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

CHIP-8

y como guía introductoria utilice

Este

.

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

Publicaciones Similares

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *