Нажатие LR перед инструкцией BL в сборке ARM - PullRequest
0 голосов
/ 05 декабря 2018

Я пытаюсь лучше понять, почему вы нажимаете LR перед вызовом BL инструкции.Я понимаю, что инструкция BL будет переходить к другой подпрограмме перед восстановлением ПК по адресу инструкции после вызова BL, но почему LR передается до вызова BL?Я написал весь рекурсивный код для факторных вычислений ниже, чтобы дать контекст.a и b - обе переменные, написанные псевдо.

LDR   RO, a
PUSH  (LR)
BL    factorial
STR   R0, b
POP   (LR)

factorial: 
CMP   RO, #0
MOVEQ R0, #1
MOVEQ PC, LR
MOV   R3, R0
SUB   R0, R0, #1
PUSH  (R3, LR)
BL    factorial
MUL   R0, R3, R0
POP   (R3, LR)
MOV   PC, LR

Я понимаю, как должна выполняться эта программа, но я не совсем понимаю, какие адреса хранятся в стеке.Ясно, что вы хотите, чтобы адрес инструкции "STR R0, b" помещался в стек после вашего первого вызова ветвления, но как он сохраняется в стеке, если LR передается до вызова BL?

Ответы [ 3 ]

0 голосов
/ 06 декабря 2018

, поскольку у меня есть удобный симулятор ...

.thumb

.globl _start
_start:
.word 0x20001000
.word reset
.word hang
.word hang
.word hang
.word hang
.word hang
.word hang

.thumb_func
reset:
    mov r0,#5
    bl test
    b hang

.thumb_func
hang:
    swi 0xFF
    b hang

test:
    cmp r0,#0
    bne test1
    bx lr
test1:
    sub r0,#1
    push {r3,lr}
    bl test
    pop {r3,pc}

build

08000000 <_start>:
 8000000:   20001000    andcs   r1, r0, r0
 8000004:   08000021    stmdaeq r0, {r0, r5}
 8000008:   08000029    stmdaeq r0, {r0, r3, r5}
 800000c:   08000029    stmdaeq r0, {r0, r3, r5}
 8000010:   08000029    stmdaeq r0, {r0, r3, r5}
 8000014:   08000029    stmdaeq r0, {r0, r3, r5}
 8000018:   08000029    stmdaeq r0, {r0, r3, r5}
 800001c:   08000029    stmdaeq r0, {r0, r3, r5}

08000020 <reset>:
 8000020:   2005        movs    r0, #5
 8000022:   f000 f803   bl  800002c <test>
 8000026:   e7ff        b.n 8000028 <hang>

08000028 <hang>:
 8000028:   dfff        svc 255 ; 0xff
 800002a:   e7fd        b.n 8000028 <hang>

0800002c <test>:
 800002c:   2800        cmp r0, #0
 800002e:   d100        bne.n   8000032 <test1>
 8000030:   4770        bx  lr

08000032 <test1>:
 8000032:   3801        subs    r0, #1
 8000034:   b508        push    {r3, lr}
 8000036:   f7ff fff9   bl  800002c <test>
 800003a:   bd08        pop {r3, pc}

и запускаю его, показывая разборку в порядке выполнения и доступ к памяти.

--- 0x08000020: 0x2005 movs r0,#0x05
--- 0x08000022: 0xF000 
--- 0x08000024: 0xF803 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FF8,0x0000)
write16(0x20000FFA,0x0000)
write16(0x20000FFC,0x0027)
write16(0x20000FFE,0x0800)
--- 0x08000036: 0xF7FF 
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FF0,0x0000)
write16(0x20000FF2,0x0000)
write16(0x20000FF4,0x003B)
write16(0x20000FF6,0x0800)
--- 0x08000036: 0xF7FF 
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FE8,0x0000)
write16(0x20000FEA,0x0000)
write16(0x20000FEC,0x003B)
write16(0x20000FEE,0x0800)
--- 0x08000036: 0xF7FF 
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FE0,0x0000)
write16(0x20000FE2,0x0000)
write16(0x20000FE4,0x003B)
write16(0x20000FE6,0x0800)
--- 0x08000036: 0xF7FF 
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000032: 0x3801 subs r0,#0x01
--- 0x08000034: 0xB508 push {r3,lr}
write16(0x20000FD8,0x0000)
write16(0x20000FDA,0x0000)
write16(0x20000FDC,0x003B)
write16(0x20000FDE,0x0800)
--- 0x08000036: 0xF7FF 
--- 0x08000038: 0xFFF9 bl 0x0800002B
--- 0x0800002C: 0x2800 cmp r0,#0x00
--- 0x0800002E: 0xD100 bne 0x08000031
--- 0x08000030: 0x4770 bx r14
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FD8)=0x0000
read16(0x20000FDA)=0x0000
read16(0x20000FDC)=0x003B
read16(0x20000FDE)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FE0)=0x0000
read16(0x20000FE2)=0x0000
read16(0x20000FE4)=0x003B
read16(0x20000FE6)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FE8)=0x0000
read16(0x20000FEA)=0x0000
read16(0x20000FEC)=0x003B
read16(0x20000FEE)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FF0)=0x0000
read16(0x20000FF2)=0x0000
read16(0x20000FF4)=0x003B
read16(0x20000FF6)=0x0800
--- 0x0800003A: 0xBD08 pop {r3,pc}
read16(0x20000FF8)=0x0000
read16(0x20000FFA)=0x0000
read16(0x20000FFC)=0x0027
read16(0x20000FFE)=0x0800
--- 0x08000026: 0xE7FF B 0x08000027
--- 0x08000028: 0xDFFF swi 0xFF

Возможно, я вижу ваше замешательство, поскольку обратный адрес для всех, кроме последнего, является одним и тем же адресом, и мы могли бы, возможно, создать пример.Но рекурсия часто имеет больше, чем адрес возврата, но имеет некоторые другие локальные переменные, которые меняются, в этом случае наша локальная переменная находится в r0, если вам не нужно сохранять ее в стеке при каждом вызове.

Первый раз, когда мы возвращаемся к началу bl после сброса:

write16(0x20000FFC,0x0027)
write16(0x20000FFE,0x0800)

В остальное время это тот же адрес возврата, но нам нужно их N в стеке, чтобы код работал так, как написано.

write16(0x20000FF4,0x003B)
write16(0x20000FF6,0x0800)

write16(0x20000FEC,0x003B)
write16(0x20000FEE,0x0800)

write16(0x20000FE4,0x003B)
write16(0x20000FE6,0x0800)

write16(0x20000FDC,0x003B)
write16(0x20000FDE,0x0800)

так что теперь мы имеем пять этих адресов в стеке.

read16(0x20000FDC)=0x003B
read16(0x20000FDE)=0x0800

...

read16(0x20000FFC)=0x0027
read16(0x20000FFE)=0x0800

В общем случае bl изменяет lr и помещает адрес возврата в стек (выше приведен код большого пальца)не код руки, но охватывает ваши вопросы, так как они работают одинаково в этом отношении).поэтому, если вы вкладываете вызовы one () вызывает два (), два () вызывает три () для two (), чтобы вернуться к одному (), то lr необходимо сохранить в two (), чтобы его можно было использовать, если выне сохраняйте lr, тогда вызов three () изменяет lr, и мы не можем вернуться.

Если ваша рекурсия хочет использовать bl (выглядит как скомпилированный код) для чистоты, и вы хотите найти способ для этой функции, factorialв моем тестовом примере, чтобы иметь возможность вернуться к исходному вызывающему, эти два факта объединяются с необходимостью помещать lr в стек.Если вы хотите добавить bl к вершине рекурсивной функции, той же самой точки входа, которую использовала внешняя вызывающая сторона, тогда каждый вызов будет добавлять lr в стек, и при каждом возврате необходимо возвращать его обратно.

ЕслиВы хотите выполнить некоторую ручную сборку, чтобы изменить ее, и она не вызывает ту же точку входа, которую вы можете избавить от bl и стека.

test:
    push {r3,lr}
test1:    
    cmp r0,#0
    beq test2
    sub r0,#1
    b test1
test2:    
    pop {r3,pc}

может даже оставить там bl *

test:
    push {r3,lr}
test1:    
    cmp r0,#0
    beq test2
    sub r0,#1
    bl test1
test2:    
    pop {r3,pc}

но если вы хотите возвращаться каждый раз, то разрыв цикла должен быть выполнен по-другому.У меня нет решения, которое использует bl и return, но может выйти из цикла в нужное время.

0 голосов
/ 06 декабря 2018

но почему LR передается до вызова BL?

Здесь вы видите стоимость рекурсии.Рекурсия выглядит просто с точки зрения кодирования более высокого уровня.Состояние сохраняется компилятором в кадре стека.Существует только один регистр LR, который подходит для конечных функций.Однако, если у вас есть расширенная цепочка вызовов, «A вызывает B, вызывает C, вызывает D», то адрес возврата «A, B и C» должен быть сохранен при выполнении в «D» с LR return to »C».Для рекурсии «A, B, C и D» - это одно и то же.

См .: Регистр ARM Link и указатель кадра для получения дополнительной информации.

Я считаю, чтопоучительно увидеть эти дополнительные инструкции.Часто вместо рекурсии может быть сформирован цикл, и линейный поток будет выполняться гораздо быстрее, с тем же количеством переменных и меньшим количеством кода.Фреймы стека и манипуляции скрыты для программистов на языках более высокого уровня.

Также обычно фрейм не нужен из-за «хвостовой рекурсии».На самом деле только первый вызов факториала должен сохранить адрес возврата, и вместо bl подойдет простой b.

0 голосов
/ 05 декабря 2018

Регистр связи LR используется для хранения адреса, по которому функция должна вернуться после завершения выполнения.Инструкция BL по сути является «вызовом»;он вычисляет адрес следующей инструкции и вставляет его в LR перед переходом.Соответствующий BX LR (ответвление на адрес, содержащийся в регистре ссылок) является 'return'.

Однако, если одна функция вызывает другую, перед выдачей инструкции BL она должна сохранить существующее значение.LR где-то, иначе он будет перезаписан и потерян навсегда.Загрузка этого в стек - самый простой способ сделать это.

Имейте в виду, что (почти) ни один код на самом деле не является "автономным".Вполне вероятно, что любой код, который вы пишете, является частью функции, даже если это main(), и поэтому регистр ссылки должен быть сохранен.

Наиболее распространенный шаблон, который вы увидите в скомпилированном коде, это то, что ссылкарегистр помещается в стек прямо вверху функции, и снова появляется только справа внизу.Кроме того, он часто просто вставляется прямо в счетчик программы, что вызывает переход без необходимости явного BX LR.Поэтому что-то вроде

.function
    ; Push working registers and LR
    PUSH {r4-r8,lr}
    ; (rest of the function goes here)
    ; Pop working registers and PC for an implicit return
    POP {r4-r8, pc}

будет типичным.

...