ARM ¿Cómo invocar la ramificación?

Estaba mirando un código con respecto al bucle.

loopinner ....
          SUBS R2,R2,#1 ; j--
          BGT loopinner ;in this case, loop should continue when j>1

En este caso, no estoy seguro de cómo BGT vuelve a ramificarse en loopinner. ¿No necesito especificar qué es mayor que? Dado que SUBS invoca las banderas, digamos si j-- se convierte en el valor de 1. ¿Cómo sabe la rama qué valor es mayor que?

Esta es su tercera pregunta simple sobre el ensamblaje de ARM en las últimas 12 horas. ¿Estás en medio de algún tipo de examen?
@ElliotAlderson no exactamente, pero recién comencé a aprender este módulo antes del cronograma planificado y estoy interesado en obtener más información sobre ARM.
Las instrucciones de bifurcación miran el estado de varias banderas establecidas por la instrucción anterior.
Tenga en cuenta que en realidad se repetirá si j>0 (usando el valor de j después del decremento), no j>1.
¿Qué parte de la documentación del brazo no entiendes? esto está claramente documentado.
@Meep ¿Estás bien? Espero que todo te vaya bien. ¡Los mejores deseos!
@jonk ¡Sí, todo bien! He estado aprendiendo por mi cuenta constantemente, ¡gracias por toda la ayuda aquí!

Respuestas (3)

A partir de los condicionales ARM, puede encontrar fácilmente que la instrucción examina los indicadores y bifurcaciones de estado Z, N y V cuando Z=0 y N=V. Dado que examina el indicador de estado V y no el indicador de estado C, esto claramente pretende ser una prueba firmada . ( Esto significa para mí que esto no es útil para el control de bucle sin firmar, para tu información ) .

Escribí esto no hace mucho tiempo, con suficiente información para entender lo que está pasando. Pero puedo resumirlo aquí.

Usemos palabras más simples de 4 bits donde solo hay 16 símbolos:

Word     Signed     Subtrahend
0000         0         1111
0001         1         1110
0010         2         1101
0011         3         1100
0100         4         1011
0101         5         1010
0110         6         1001
0111         7         1000
1000        -8         0111
1001        -7         0110
1010        -6         0101
1011        -5         0100
1100        -4         0011
1101        -3         0010
1110        -2         0001
1111        -1         0000

Arriba, la tercera columna es lo que la ALU realmente usa al restar ese valor. Simplemente invierte cada bit antes de agregar. (La ALU nunca resta nada. Ni siquiera sabe cómo). Entonces, la instrucción SUB en realidad realiza la suma, usando la forma de sustraendo del valor al sumar. (Si desea comprender la semántica de los bits de estado, es muy importante que domine este concepto, ya que lo ayudará cuando de lo contrario estaría confundido).

Pégalo en tu frente...

UNA CPU SOLO AÑADE. NO SE PUEDE RESTAR .

Si alguna vez siente la tentación de seguir el camino primitivo de creer que cualquier tipo de instrucción de resta en realidad resta, y esto incluye todas las instrucciones de comparación que establecen bits de estado pero no cambian los valores de registro, simplemente patéese muy duro, muy rápido. no sucede

UNA CPU SOLO AÑADE. NO SE PUEDE RESTAR .

Todo tiene que ser moldeado en semántica de adición. Todo.

Un SUBS R2, R2, #1 , en este universo de 4 bits que acabo de crear, agregaría 1110 más un arrastre de 1 también. Solo hay 16 posibilidades:

Actual Operation    Operation Result    Operation      Comparison
 R2     SUBS OP        Z N V C ALU       Semantics      Semantics   Z=0 & N=V?
0000 + 1110 + 1        0 1 0 0 1111     0 - 1 = -1       0 > 1 ?    False
0001 + 1110 + 1        1 0 0 1 0000     1 - 1 =  0       1 > 1 ?    False
0010 + 1110 + 1        0 0 0 1 0001     2 - 1 =  1       2 > 1 ?    True
0011 + 1110 + 1        0 0 0 1 0010     3 - 1 =  2       3 > 1 ?    True
0100 + 1110 + 1        0 0 0 1 0011     4 - 1 =  3       4 > 1 ?    True
0101 + 1110 + 1        0 0 0 1 0100     5 - 1 =  4       5 > 1 ?    True
0110 + 1110 + 1        0 0 0 1 0101     6 - 1 =  5       6 > 1 ?    True
0111 + 1110 + 1        0 0 0 1 0110     7 - 1 =  6       7 > 1 ?    True
1000 + 1110 + 1        0 0 1 0 0111    -8 - 1 = -9 E    -8 > 1 ?    False
1001 + 1110 + 1        0 1 0 1 1000    -7 - 1 = -8      -7 > 1 ?    False
1010 + 1110 + 1        0 1 0 1 1001    -6 - 1 = -7      -6 > 1 ?    False
1011 + 1110 + 1        0 1 0 1 1010    -5 - 1 = -6      -5 > 1 ?    False
1100 + 1110 + 1        0 1 0 1 1011    -4 - 1 = -5      -4 > 1 ?    False
1101 + 1110 + 1        0 1 0 1 1100    -3 - 1 = -4      -3 > 1 ?    False
1110 + 1110 + 1        0 1 0 1 1101    -2 - 1 = -3      -2 > 1 ?    False
1111 + 1110 + 1        0 1 0 1 1110    -1 - 1 = -2      -1 > 1 ?    False

En Operation Result , tengo una columna para ALU . El campo ALU es lo que regresa a R2 después de que se completa la instrucción SUBS. (La bandera de estado V es generada por un XOR del acarreo del siguiente bit más significativo durante la operación y el bit de acarreo en sí). Tenga en cuenta también que hay un solo caso marcado con E donde ocurrió un desbordamiento firmado .

Ahora puede ver fácilmente por qué la instrucción BGT aplica esos bits de estado en particular exactamente de la manera en que lo hace. Es cierto que esto usa palabras de 4 bits. Pero exactamente la misma idea se aplica a tamaños de palabra mucho más amplios, sin ningún cambio.

Al volver a mirar la tabla, puede ver que la condición es Verdadera si y solo si R2 era 2 o mayor antes de la resta, y no 1, 0 o menor.

Tu pregunta:

¿No necesito especificar qué es mayor que? Dado que SUBS invoca las banderas, digamos si j-- se convierte en el valor de 1. ¿Cómo sabe la rama qué valor es mayor que?

Comencemos con la siguiente tabla del Manual de referencia de la arquitectura ARMv6-M , página A6-99:

ingrese la descripción de la imagen aquí

La condición GT se describe como " Firmado mayor que ". La razón por la que la documentación no especifica una constante es que esta prueba ocurre después de alguna instrucción previa. Esa instrucción previa define el contexto. Pero sin tener ese contexto, todo lo que se puede decir es un signo > general .

Entonces, si la instrucción anterior fuera CMP:

ingrese la descripción de la imagen aquí

Entonces, el contexto sería la comparación de dos valores con signo y la instrucción BGT significaría "bifurcarse cuando el operando con signo 1 es mayor que el operando con signo 2".

Pero en su caso, con "SUBS R2, R2, #1", el contexto cambia y la instrucción BGT significaría "branch mientras R2 firmado sigue siendo mayor que 0".

La instrucción de bifurcación condicional en sí misma no sabe cuál era la instrucción anterior. Tampoco sabe qué registro(s) están involucrados. Ese conocimiento se deja al individuo (o compilador) que está generando el flujo de instrucciones. Entonces, la instrucción de bifurcación en realidad no tiene un valor constante fijo, ni tiene un registro con el cual comparar. Depende completamente de lo que hicieron las instrucciones anteriores con los bits de estado. Simplemente examina el estado resultante y luego hace lo que hace. Depende de usted conocer el contexto y usarlo correctamente.

(Hablando de eso, el comentario del código fuente puede ser engañoso o incorrecto).

Nota

Elliot discrepa (ver la discusión a continuación) sin evidencia. Él escribe: "Podría argumentar de manera equivalente que una CPU solo puede restar". Él puede hacer ese argumento, pero es sólo académico. El hecho real del asunto es que las CPU no restan. Agregan.

Entonces, si bien esta es en parte mi respuesta, brindando evidencia clara e inequívoca en apoyo para que incluso Elliot pueda comprender la situación en el terreno, hoy también es una excelente continuación. Así que estoy muy contento por la oportunidad que me brinda Elliot de ampliar la discusión.

Mi primera CPU se fabricó con 7400 piezas que construí y completé con éxito en 1974. Los reporteros de los periódicos, para mi sorpresa, aparecieron y escribieron un artículo al respecto. Esa es mi primera experiencia. Desde entonces, trabajé profesionalmente en Intel realizando pruebas de conjuntos de chips para el conjunto de chips BX y, como cuestión relevante para la enseñanza de esta materia, he impartido clases de arquitectura informática como profesor adjunto en la Universidad Estatal de Portland en la década de 1990, con tamaños de clase de aproximadamente 65-75 estudiantes. Esta es la universidad de 4 años más grande del estado de Oregón.

Siento ambigüedad (que expresa ambivalencia sobre cómo se pueden hacer los cálculos) sobre cómo los procesadores generan sus bits de estado y cómo calculan solo lleva a los estudiantes a una incertidumbre, confusión y dificultad innecesarias que pueden tardar horas, semanas, meses y, a veces, incluso años en corregirse. Así como enseñar álgebra abstracta de teoría de grupos antes de transmitir los conceptos básicos confundiría a la mayoría de los estudiantes de álgebra de primer año, también lo haría enseñar abstracciones académicas sobre cómo las computadoras pueden hacer cosas. Más estudiantes serían dañados que ayudados.

La simple verdad es que la decodificación de instrucciones emite un ADD, incluso cuando el texto de la instrucción (después de todo, es solo texto, no es lo que realmente está sucediendo) dice SUB. La decodificación todavía emite un ADD. Simplemente modifica algunos detalles del operando en el camino.

Del mismo modo, como también debe ser en el caso del procesador ARM, la teoría anterior es todo lo que necesita para comprender cómo se hacen realmente las cosas.

¡Por favor, no te confundas! Las computadoras agregan. No restan. Simplemente juguetean un poco para que parezca que restan.

Para bien o para mal, es importante comprender qué hace realmente una computadora para comprender ciertos bits de estado; lo que hacen y por qué lo hacen. No hay otra forma de evitarlo. El modelo teórico anterior es la forma en que funcionan las cosas en los procesadores modernos y es cómo resolver y comprender los bits de estado correctamente. Hay una buena razón por la que las cosas son como son.

Espero que estos detalles, arriba, y los que escribiré a continuación, sean útiles. Cualquier falta de comunicación aquí es mía y con gusto trabajaré para reparar, enmendar y mejorar este documento donde pueda.

Para continuar, usaré el Manual de referencia de la arquitectura ARMv6-M como referencia.

Comencemos en la página A6-187 (caso de registro):

ingrese la descripción de la imagen aquí

Aquí puede ver que documentan claramente este comportamiento:

AddWithCarry(R[n], NOT(shifted), '1')

Esta es una suma, con el operando 2 (el sustraendo) invertido y el acarreo establecido en '1'. Justo como escribí sucede, arriba. (Así es como se hace).

En el caso de extensiones de varias palabras, vaya a la página A6-173 y busque SBCS:

ingrese la descripción de la imagen aquí

Aquí tenga en cuenta que de nuevo usan la suma:

AddWithCarry(R[n], NOT(shifted), APSR.C)

En lugar de que el acarreo sea un '1' codificado de forma rígida, como lo es para la instrucción SUBS, ahora está usando el último valor de acarreo guardado. En este caso, normalmente se espera que se lleve a cabo desde una instrucción SUBS (o SBCS) anterior.

Para operaciones de múltiples palabras, uno comienza con SUBS (o ADDS) y luego continúa el proceso con SBCS (o ADCS) posteriores, que utilizan la ejecución de instrucciones anteriores para respaldar una operación de múltiples palabras.

En la suma de varias palabras, esta comida para llevar se puede considerar simplemente como una comida para llevar , y lo es. Un '1' indica que se produjo un acarreo y que debe solucionarse. Un '0' indica que no se produjo ningún acarreo.

En el caso de la resta de varias palabras, esta transferencia se ve mejor como un préstamo invertido . Un '1' indica que no hubo necesidad de tomar prestado de una palabra de orden superior. Un '0' indica que hay necesidad de pedir prestado. Dado que una instrucción SUBS siempre establece esto en '1', esto significa que no hay préstamo (el resultado de la resta requiere un 'incremento' para compensar el operando invertido 2). Pero para la instrucción SBCS, si APSR.C es un ' 0', entonces no se produce ningún 'incremento' y esto es lo mismo que pedir prestado (dado que se requiere un incremento, si no hay préstamo).

La instrucción ADCS, que se encuentra en la página A6-106 pero que no se muestra aquí, también utiliza la realización de ejecuciones de instrucciones anteriores. No invierte el valor de ejecución ni hace algo extraño o diferente, solo porque es una instrucción ADCS. Hace exactamente lo mismo que la instrucción SBCS, excepto y solo por un detalle menor: la instrucción SBCS invertirá el operando 2 y ADCS no. Eso es todo.

Este es uno de los aspectos realmente geniales sobre la forma en que funcionan estos detalles. Se requiere muy poca lógica adicional para convertir una suma en una resta y/o una suma de varias palabras en una resta de varias palabras.

Y finalmente, para completar la historia, vea la página A2-35:

ingrese la descripción de la imagen aquí

De acuerdo con mis descripciones de cómo funcionan realmente las cosas, arriba.

Es realmente un placer ver cómo funciona todo esto. Vale la pena jugar con diferentes valores con y sin signo y, a mano, configurar y usar banderas de estado. Realmente profundiza estas ideas. ¡Y son muy buenos!

Todo lo anterior se trata de comprender los bits de estado y cómo se generan y por qué se generan de la forma en que se generan. Si te enfocas en lo que realmente sucede en una CPU, el resto simplemente cae como las consecuencias necesarias y es muy fácil de entender, entonces.

Una CPU solo suma. No puede restar.

No tiene sentido insistir dogmáticamente en que una CPU solo puede sumar. De manera equivalente, podría argumentar que una CPU solo puede restar, y que para "agregar" niega el segundo operando y resta. La ALU es solo una gota de lógica combinacional, optimizada para tiempo, área y potencia. Si implemento un sumador/restador en un FPGA, obtengo un multiplexor que realiza sumas y restas, y ninguna operación es más fundamental que la otra.
@ElliotAlderson Solía ​​darse el caso de que podían hacer las cosas de varias maneras y, en esos días, comprender el estado era más complicado de lo que se ha vuelto hoy. Había que leer el manual y ver qué hacía algún diseñador esa vez. Pero hoy en día, lo único que hacen es sumar. Lo cual es algo bueno , no algo malo. La razón por la que estoy haciendo este énfasis es porque, una vez que la idea se perfora profundamente en el cerebro, entonces todo tiene sentido , siempre y puedes analizar cualquier cosa con precisión. Menos memorización, más comprensión. Es mejor de esta forma.
@ElliotAlderson Todo lo que escribí, por cierto, es absolutamente exacto.
Bueno, estaremos de acuerdo en estar en desacuerdo entonces. En mi experiencia personal, asumir que tengo un conocimiento absoluto por lo general será contraproducente.
@ElliotAlderson Aceptado. Gracias. Y estarías muy en lo correcto "en el pasado". No puedo contar la cantidad de elecciones extrañas hechas por diseñadores de CPU al azar. Continuamente investigaba los detalles de los manuales porque no parecía que dos personas tomaran la misma decisión. No hay desacuerdo allí. Pero trabajé en Intel a fines de la década de 1990 en el diseño y las pruebas del conjunto de chips P II y en ese momento obtuve mi opinión sobre cómo todos están ahora en la misma página en esta elección de diseño de CPU en particular. Es cierto. Si alguna vez encuentra una excepción ALU moderna a lo que escribí, proporciónela y eliminaré mi respuesta aquí.
@ElliotAlderson Además, y más concretamente, con ARM es exactamente cierto.
Bueno, entonces me disculpo, no sabía que había diseñado las ALU en todas las implementaciones de silicio ARM, así como los núcleos blandos provistos para la implementación en FPGA. Sin embargo, creo que mi ejemplo de implementar una ALU en un FPGA sigue en pie y he aprendido que estas discusiones realmente no son fructíferas. Paz.
@ElliotAlderson Sabes que tengo razón. Ser inequívoco es mejor en este tema. Y si por "ser fructífero" te refieres a argumentarme para cambiar una visión correcta para la cual no hay evidencia en contra como una pérdida de tiempo, eso es bastante cierto. Pero nuevamente, si me proporciona un solo ejemplo moderno como evidencia, admitiré mi falla y también eliminaré la publicación. Obtendrás todo lo que quieras. Sólo encuentra la evidencia. Estoy muy abierto a equivocarme. Solo sé que no lo soy. Y no te gusta mi certeza. Oh bien.
En el contexto de Arm (según la pregunta), @jonk es arquitectónicamente correcto. Los diversos ArmARM se definen SUBScomo AddWithCarry(R[n], NOT(imm32), '1')(puedo expandir esto si fuera útil), que cubre esta respuesta.
@ElliotAlderson Consulte la actualización. awjlogan me dio pistas sobre cómo encontrar los documentos correctos. (Apreciado.) Está todo ahí.
@jonk: eso es exactamente, lo siento, demasiado lento de mi parte :)
"Podría argumentar de manera equivalente que una CPU solo puede restar": tal vez, pero estarías solo. La resta en ALU se ha implementado como "negar el segundo operando, luego agregar" desde al menos el 6502, probablemente hasta el 8008, tal vez incluso el 4004 o antes. El circuito fundamental involucrado se llama "sumador". Cualquier circuito de "resta" comparable es más complejo que un sumador, y es fundamentalmente un sumador con un negador (que también es un sumador) delante de él (ya que la negación del complemento a dos es "invertir bits" y sumar uno ) . Lo siento, hombre, fundamentalmente es todo suma.
@ElliotAlderson Escribí: "Si me proporciona un solo ejemplo moderno como evidencia, admitiré mi fracaso ..." porque aceptar una buena evidencia y admitir un error es importante para mí y define lo que quiero ser para los demás. Su respuesta, "Bueno, entonces me disculpo, no sabía que había diseñado las ALU en todas las implementaciones de silicio ARM...", no habría sido capaz de escribir. No está en mis huesos. Si se invirtieran las cosas, estaría haciendo una disculpa fuerte y clara, admitiendo el error y prometiendo hacerlo mejor. ¿Dejas que esto se convierta en el comportamiento que te define? Espero que no.
¿Alguien está cuestionando seriamente si la lógica de suma se usa para restar? Pensé en este punto que todos los que realmente trabajan en esto o se toman unos minutos para mirar lo saben. No es realmente relevante si el escritor técnico de algunos documentos eligió indicar eso o no, lo que importa directamente es lo que hizo el compilador del lenguaje, la biblioteca de celdas o el autor del código.
@old_timer Sí. Elliot me rebajó y luego se defendió con argumentos tontos que no tienen base en los hechos. Él solo me estaba intimidando. Pero Elliot y yo tenemos visiones del mundo fundamentalmente contradictorias sobre la enseñanza (de discusiones anteriores). Ambos hemos sido maestros; él dice que todavía lo es. Y también quiere que me ajuste a sus propias reglas personales sobre la enseñanza aquí. Larga discusión en la que tuvimos que estar en desacuerdo y dejarlo ahí. Sospecho que se trataba más de y de sus continuos esfuerzos por intimidarme. Y nada más, para ser honesto. Ahora se ha dejado expuesto.
@jonk ahh, sí, hay otros usuarios con los que tengo problemas similares. Sí bastante expuesto.

En su ejemplo de código, la SUBinstrucción tiene un Ssufijo, esto significa que la subinstrucción establecerá los indicadores de condición, que evaluará BGT. Para que se tome la rama, la Zbandera debe ser 0 y la Nbandera debe ser igualV

Sé que la subinstrucción establecerá los indicadores de condición, donde N = no negativo, Z = no cero, C/V = desbordamiento sin signo/con signo. Pero, ¿cómo sabe la rama, en este caso, cuándo ir al bucle interno cuando, por ejemplo, j=2, después de decrementar es 1, por lo tanto, N=0, Z=0, C/V=0, cómo BGT (¿mayor que?) vuelve al bucle otra vez?
¿Por qué será cero si R2 todavía tiene un valor de 1? En cuanto a BGT, ¿con qué se compara? ¿Mas grande que? Perdón por estas preguntas, nuevo en ARM.
Los comandos cambian de bandera. La verificación de banderas permite la prueba condicional. Utilice la C para explicar el asesor. Si j > 0, bifurca.
@Meep Si fuera igual, Z sería 1, si fuera menor, no estoy del todo seguro, pero probablemente C sería 1. Dado que ambos son cero, debe ser mayor.
Esto es correcto. Está comprobando si es mayor que cero, en ese escenario ambas banderas C, V tendrán el mismo valor y Z será 0.

La documentación del brazo establece claramente que GT es un signo mayor que, se bifurcará cuando Z==0,N==V.

Cuando r2 = 2. Recuerde de la escuela primaria que x - y = x + (-y), y desde el primer día (o poco después) en ingeniería informática/ciencia/cualquiera que sea la negación del complemento a dos es invertir y sumar uno, de modo que x - y = x + (~y) + 1. Esto ahorra en lógica y es cómo hacemos la resta

      1  add one
   0010 
 + 1110  invert
==========

cuatro bits es más que suficiente para ver lo que está pasando, el resultado es el mismo que con 32 bits.

   11101
    0010 
  + 1110
 ==========
    0001

Entonces N = 0 y Z = 0 del resultado. El acarreo y el acarreo del msbit son los mismos, por lo que V = 0 (xor del acarreo y el acarreo del msbit, también puede hacerlo mediante la inspección de los msbits de los operandos y el resultado).

Necesitamos Z == 0 y N == V para hacer la bifurcación, y lo son, por lo que sucede la bifurcación.

Encontrará que este es el caso de los números positivos, ya que este es un signo mayor que, si desea un signo mayor que, entonces use bcs/bhs, la lógica funciona igual, solo se optimiza para usar solo llevar a cabo (puede ver esto también si miras la tabla jonk generada o generas una tú mismo)

Cuando r2 = 1

   11111
    0001
 +  1110
 ==========
    0000

Z = 1, N = 0, V = 0

N == V pero Z != 0 por lo que la bifurcación no sucede.

versión corta de la respuesta jonks, que muestra cómo obtenemos N, V, Z. upvote jonks si / cuando / en su lugar votas a favor de este.