Saya mencoba menulis kode Majelis untuk fungsi berikut:

#include <iostream>
void f(int x) {
    if (x > 0) {
        std::cout << x << std::endl;
        f(x-1);
        std::cout << x << std::endl;
    }
}
int main() {
    f(1);
}

Output dari skrip fungsi ini adalah 1 1. Saya mencoba menulis kode perakitan untuk apa yang disebut assembler "komputer murah", komputer yang ditemukan oleh Anthony Dos Reis untuk bukunya "C and C++ under the hood". Kode Majelis yang saya tulis adalah:

startup   jsr main
          halt             ; back to operating system
;==============================================================
                           ; #include <stdio.h>
greater   dout
          nl
          sub r1, r0, 1
          push lr
          push fp
          mov fp, sp
          push r1
          jsr f
          add sp, sp, 1
          dout
          nl
          mov sp, fp
          pop fp
          pop lr
          ret
;==============================================================
f         push lr          ; int f()
          push fp          ; {
          mov fp, sp
          ldr r0, fp, 2
          cmp r0, 0
          brgt greater
          mov sp, fp
          pop fp
          pop lr
          ret
;==============================================================
main      push lr
          push fp
          mov fp, sp
          mov r0, 1
          push r0
          jsr f
          add sp, sp, 1
          mov sp, fp
          pop fp
          pop lr
          ret

Kode mencetak 1 0 ke stdout, dan jelas salah. Alasan untuk keluaran palsu terletak pada kenyataan bahwa register r0 berisi 1 sebelum ia melompat ke fungsi f selama evaluasi cabang greater, dan kemudian fungsi f memodifikasi register dan menetapkan r0 ke 0 saat melakukan perbandingan cmp. Ini membuat saya bertanya-tanya bagaimana saya assembler dapat menjaga register tetap invarian selama panggilan fungsi. Saya memikirkan hal berikut:

  1. Dengan hanya mendorong seluruh register ke tumpukan dan memuatnya lagi setelahnya
  2. Dari entah bagaimana mengumpulkan apa yang dipanggil fungsi dan kemudian "melindungi" register jika perlu.

Saya pikir solusi 1 sangat defensif dan kemungkinan mahal, sementara solusi 2 mungkin rumit dan membutuhkan banyak pengetahuan. Kemungkinan besar, saya hanya membuat kesalahan dalam menulis Majelis, tetapi saya masih tidak mengerti bagaimana assembler dapat menjaga registernya tetap invarian ketika perlu dalam kasus umum, atau bagaimana seseorang dapat mengatasi masalah seperti yang diuraikan di atas. Adakah yang tahu apa yang harus dilakukan dalam kasus ini? Saya berterima kasih atas bantuan atau saran apa pun!

1
fabian 8 Januari 2021, 22:14

2 jawaban

Jawaban Terbaik

Umumnya, platform komputasi menyertakan Application Binary Interface (ABI) yang menetapkan, antara lain, protokol untuk pemanggilan fungsi. ABI menetapkan bahwa register prosesor tertentu digunakan untuk meneruskan argumen atau mengembalikan hasil, register tertentu dapat digunakan secara bebas oleh rutin yang disebut (register awal atau volatil), register tertentu dapat digunakan oleh rutin yang dipanggil tetapi harus dikembalikan ke nilai aslinya (register yang diawetkan atau non-volatile), dan/atau register tertentu tidak boleh diubah oleh rutin yang disebut. Aturan tentang rutinitas panggilan juga bisa disebut "konvensi panggilan."

Jika rutin Anda menggunakan salah satu register yang memanggil fungsi bebas untuk digunakan, dan ingin nilai register itu dipertahankan di seluruh pemanggilan fungsi, ia harus menyimpannya sebelum pemanggilan fungsi dan mengembalikannya sesudahnya.

Umumnya, ABI dan fungsi mencari keseimbangan di mana register yang mereka gunakan. Jika ABI mengatakan semua register harus disimpan dan dipulihkan, maka fungsi yang dipanggil harus menyimpan dan memulihkan setiap register yang digunakannya. Sebaliknya, jika ABI mengatakan tidak ada register yang harus disimpan dan dipulihkan, maka fungsi panggilan harus menyimpan dan memulihkan setiap register yang diperlukan. Dengan campuran, rutinitas panggilan mungkin sering memiliki beberapa datanya dalam register yang disimpan (sehingga tidak harus menyimpannya sebelum panggilan dan mengembalikannya sesudahnya), dan rutinitas yang dipanggil tidak akan menggunakan semua register yang harus melestarikan (sehingga tidak harus menyimpan dan memulihkannya; melainkan menggunakan register awal), sehingga jumlah keseluruhan register yang disimpan dan dipulihkan yang dilakukan berkurang.

3
Eric Postpischil 8 Januari 2021, 20:19

Kombinasi Arsitektur/Platform seperti Windows-on-x64, atau Linux-on-ARM32, memiliki apa yang disebut ABI, atau Antarmuka Biner Aplikasi.

ABI menentukan dengan tepat bagaimana register digunakan, bagaimana fungsi dipanggil, bagaimana pengecualian bekerja, dan bagaimana argumen fungsi dilewatkan. Aspek penting dari ini adalah, selama pemanggilan fungsi, register apa yang harus disimpan, dan siapa yang menyimpannya, dan register apa yang dapat dimusnahkan.

Register yang dapat dimusnahkan selama pemanggilan fungsi disebut register volatil. Register yang harus dipertahankan oleh pemanggilan fungsi disebut register non-volatile. Biasanya, jika pemanggil ingin mempertahankan register volatil, pemanggil mendorong nilai ke tumpukan, memanggil fungsi, dan kemudian mengeluarkannya saat selesai. Jika fungsi yang dipanggil (callee) ingin menggunakan register non-volatile, ia harus menyimpannya dengan cara yang sama.

Baca konvensi panggilan Windows x64 di sini.

2
Tumbleweed53 8 Januari 2021, 19:51