GNU/Linux >> Belajar Linux >  >> Linux

Bagian mana dari kode rakitan HelloWorld ini yang penting jika saya menulis program dalam rakitan?

Batas minimum mutlak yang akan berfungsi pada platform seperti ini adalah

        .globl main
main:
        pushl   $.LC0
        call    puts
        addl    $4, %esp
        xorl    %eax, %eax
        ret
.LC0:
        .string "Hello world"

Tapi ini melanggar sejumlah persyaratan ABI. Nilai minimum untuk program yang sesuai dengan ABI adalah

        .globl  main
        .type   main, @function
main:
        subl    $24, %esp
        pushl   $.LC0
        call    puts
        xorl    %eax, %eax
        addl    $28, %esp
        ret
        .size main, .-main
        .section .rodata
.LC0:
        .string "Hello world"

Segala sesuatu yang lain dalam file objek Anda adalah kompiler yang tidak mengoptimalkan kode seketat mungkin, atau opsional anotasi untuk ditulis ke file objek.

.cfi_* arahan, khususnya, adalah anotasi opsional. Mereka diperlukan jika dan hanya jika fungsi mungkin ada di tumpukan panggilan ketika pengecualian C++ dilemparkan, tetapi mereka berguna dalam program apa pun dari mana Anda mungkin ingin mengekstrak jejak tumpukan. Jika Anda akan menulis kode nontrivial dengan tangan dalam bahasa rakitan, mungkin ada baiknya mempelajari cara menulisnya. Sayangnya, mereka didokumentasikan dengan sangat buruk; Saat ini saya tidak menemukan apa pun yang menurut saya layak untuk ditautkan.

Garis

.section    .note.GNU-stack,"",@progbits

juga penting untuk diketahui jika Anda menulis bahasa rakitan dengan tangan; itu adalah anotasi opsional lainnya, tetapi yang berharga, karena artinya adalah "tidak ada apa pun dalam file objek ini yang mengharuskan tumpukan untuk dapat dieksekusi." Jika semua file objek dalam program memiliki anotasi ini, kernel tidak akan membuat tumpukan dapat dieksekusi, yang sedikit meningkatkan keamanan.

(Untuk menunjukkan bahwa Anda lakukan membutuhkan tumpukan agar dapat dieksekusi, Anda meletakkan "x" bukannya "" . GCC dapat melakukan ini jika Anda menggunakan ekstensi "fungsi bersarang". (Jangan lakukan itu.))

Mungkin perlu disebutkan bahwa dalam sintaks rakitan "AT&T" yang digunakan (secara default) oleh GCC dan GNU binutils, ada tiga jenis baris:Sebuah baris dengan satu token di atasnya, diakhiri dengan titik dua, adalah sebuah label. (Saya tidak ingat aturan tentang karakter apa yang dapat muncul di label.) Baris yang pertama token dimulai dengan titik, dan tidak diakhiri dengan titik dua, adalah semacam arahan untuk assembler. Yang lainnya adalah instruksi perakitan.


terkait:Bagaimana cara menghilangkan "kebisingan" dari keluaran rakitan GCC/dentang? .cfi arahan tidak berguna secara langsung bagi Anda, dan program akan bekerja tanpa mereka. (Info stack-unwind diperlukan untuk penanganan pengecualian dan pelacakan balik, jadi -fomit-frame-pointer dapat diaktifkan secara default. Dan ya, gcc memancarkan ini bahkan untuk C.)

Sejauh jumlah baris sumber asm yang diperlukan untuk menghasilkan nilai program Hello World, jelas kami ingin menggunakan fungsi libc untuk melakukan lebih banyak pekerjaan untuk kami.

Jawaban @Zwol memiliki implementasi terpendek dari kode C asli Anda.

Inilah yang dapat Anda lakukan dengan tangan , jika Anda tidak peduli dengan status keluar dari program Anda, cukup cetak string Anda.

# Hand-optimized asm, not compiler output
    .globl main            # necessary for the linker to see this symbol
main:
    # main gets two args: argv and argc, so we know we can modify 8 bytes above our return address.
    movl    $.LC0, 4(%esp)     # replace our first arg with the string
    jmp     puts               # tail-call puts.

# you would normally put the string in .rodata, not leave it in .text where the linker will mix it with other functions.
.section .rodata
.LC0:
    .asciz "Hello world"     # asciz zero-terminates

C yang setara (Anda baru saja meminta Hello World terpendek, bukan yang memiliki semantik identik):

int main(int argc, char **argv) {
    return puts("Hello world");
}

Status keluarnya ditentukan oleh implementasi tetapi pasti dicetak. puts(3) mengembalikan "angka non-negatif", yang mungkin berada di luar kisaran 0..255, jadi kami tidak dapat mengatakan apa pun tentang status keluar program menjadi 0 / bukan nol di Linux (di mana status keluar proses adalah 8 rendah). bit integer diteruskan ke exit_group() system call (dalam hal ini dengan kode startup CRT yang disebut main()).

Menggunakan JMP untuk mengimplementasikan tail-call adalah praktik standar, dan biasanya digunakan ketika suatu fungsi tidak perlu melakukan apa pun setelah fungsi lain kembali. puts() pada akhirnya akan kembali ke fungsi yang disebut main() , seperti jika put() telah kembali ke main() dan kemudian main() telah kembali. pemanggil main() masih harus berurusan dengan args yang diletakkan di tumpukan untuk main(), karena mereka masih ada (namun dimodifikasi, dan kami diizinkan untuk melakukannya).

gcc dan dentang tidak menghasilkan kode yang memodifikasi ruang arg-passing pada stack. Namun, ini sangat aman dan sesuai dengan ABI:fungsi "memiliki" arg mereka di stack, meskipun const . Jika Anda memanggil suatu fungsi, Anda tidak dapat berasumsi bahwa argumen yang Anda masukkan ke tumpukan masih ada. Untuk melakukan panggilan lain dengan arg yang sama atau serupa, Anda harus menyimpan semuanya lagi.

Perhatikan juga bahwa ini memanggil puts() dengan penyelarasan tumpukan yang sama dengan yang kami miliki saat masuk ke main() , jadi sekali lagi kami mematuhi ABI dalam mempertahankan penyelarasan 16B yang diperlukan oleh versi modern dari x86-32 alias i386 System V ABI (digunakan oleh Linux).

.string string zero-terminates, sama seperti .asciz , tetapi saya harus mencarinya untuk memeriksa. Saya akan merekomendasikan hanya menggunakan .ascii atau .asciz untuk memastikan Anda jelas apakah data Anda memiliki byte terminasi atau tidak. (Anda tidak memerlukannya jika Anda menggunakannya dengan fungsi panjang eksplisit seperti write() )

Di x86-64 System V ABI (dan Windows), arg diteruskan dalam register. Hal ini membuat pengoptimalan tail-call jauh lebih mudah, karena Anda dapat mengatur ulang args atau meneruskan lainnya args (selama Anda tidak kehabisan register). Ini membuat kompiler bersedia melakukannya dalam praktik. (Karena seperti yang saya katakan, mereka saat ini tidak suka membuat kode yang mengubah ruang arg yang masuk di stack, meskipun ABI jelas bahwa mereka diizinkan, dan fungsi yang dihasilkan kompiler berasumsi bahwa callees mengalahkan argumen stack mereka .)

dentang atau gcc -O3 akan melakukan pengoptimalan ini untuk x86-64, seperti yang Anda lihat di penjelajah kompiler Godbolt :

#include <stdio.h>
int main() { return puts("Hello World"); }

# clang -O3 output
main:                               # @main
    movl    $.L.str, %edi
    jmp     puts                    # TAILCALL

 # Godbolt strips out comment-only lines and directives; there's actually a .section .rodata before this
.L.str:
    .asciz  "Hello World"

Alamat data statis selalu sesuai dengan ruang alamat 31 bit yang rendah, dan dapat dieksekusi tidak memerlukan kode yang tidak tergantung posisi, jika tidak, mov akan menjadi lea .LC0(%rip), %rdi . (Anda akan mendapatkan ini dari gcc jika dikonfigurasi dengan --enable-default-pie untuk membuat executable posisi-independen.)

Cara memuat alamat fungsi atau label ke dalam register di GNU Assembler

Halo Dunia menggunakan Linux x86 32-bit int 0x80 panggilan sistem secara langsung, tanpa libc

Lihat Halo, dunia dalam bahasa rakitan dengan panggilan sistem Linux? Jawaban saya di sana awalnya ditulis untuk SO Docs, kemudian dipindahkan ke sini sebagai tempat untuk meletakkannya ketika SO Docs ditutup. Itu tidak benar-benar cocok di sini, jadi saya memindahkannya ke pertanyaan lain.

terkait:Tutorial Angin Puyuh tentang Membuat Eksekusi ELF yang Sangat Kecil untuk Linux. File biner terkecil yang dapat Anda jalankan yang baru saja melakukan exit() system call. Itu tentang meminimalkan ukuran biner, bukan ukuran sumber atau bahkan hanya jumlah instruksi yang benar-benar dijalankan.


Linux
  1. MySQL vs. MariaDB:Apa Perbedaan Utama Antara Mereka?

  2. Apa yang rentan tentang kode C ini?

  3. Apa itu program CLI standar untuk mengelola pengguna dan grup?

  1. Linux – Bagian Kernel yang Dimiliki Atau Tertutup?

  2. Apa saja Jenis Shell yang Berbeda di Linux?

  3. Apa manfaat CloudLinux?

  1. Apa Jenis-Jenis Server DNS

  2. Apa itu Kode Keluar Bash di Linux

  3. Ketika assert() gagal, apa kode keluar programnya?