تشغيل رمز التجميع 32 بت على معالج Linux 64 بت ومعالج 64 بت: اشرح الشذوذ
سؤال
أنا في مشكلة مثيرة للاهتمام. لقد نسيت أنني أستخدم آلة ونظام التشغيل 64 بتات وكتبت رمز تجميع 32 بت. لا أعرف كيف أكتب كود 64 بت.
هذا هو رمز التجميع x86 32 بت لـ GNU Assembler (AT&T Syntax) على Linux.
//hello.S
#include <asm/unistd.h>
#include <syscall.h>
#define STDOUT 1
.data
hellostr:
.ascii "hello wolrd\n";
helloend:
.text
.globl _start
_start:
movl $(SYS_write) , %eax //ssize_t write(int fd, const void *buf, size_t count);
movl $(STDOUT) , %ebx
movl $hellostr , %ecx
movl $(helloend-hellostr) , %edx
int $0x80
movl $(SYS_exit), %eax //void _exit(int status);
xorl %ebx, %ebx
int $0x80
ret
الآن ، يجب أن يعمل هذا الرمز بشكل جيد على معالج 32 بت ونظام التشغيل 32 بت صحيح؟ كما نعلم أن 64 بت معالجات متوافقة مع معالجات 32 بت. لذلك ، لن يكون هذا أيضًا مشكلة. تنشأ المشكلة بسبب الاختلافات في مكالمات النظام وآلية الاتصال في نظام التشغيل OS و 32 بت 64 بت. لا أعرف لماذا لكنهم غيروا أرقام مكالمات النظام بين Linux 32 بت و 64 بت.
يحدد ASM/unistd_32.h:
#define __NR_write 4
#define __NR_exit 1
ASM/UNISTD_64.H يحدد:
#define __NR_write 1
#define __NR_exit 60
على أي حال باستخدام وحدات الماكرو بدلاً من الأرقام المباشرة. ضمان أرقام استدعاء النظام الصحيحة.
عندما أقوم بتجميع وربط البرنامج.
$cpp hello.S hello.s //pre-processor
$as hello.s -o hello.o //assemble
$ld hello.o // linker : converting relocatable to executable
انها لا طباعة helloworld
.
في GDB عرضه:
- خرج البرنامج بالرمز 01.
لا أعرف كيف أقوم بالتصحيح في GDB. باستخدام البرنامج التعليمي ، حاولت تصحيحه وتنفيذ التعليمات عن طريق التحقق من التعليمات في كل خطوة. إنه يظهر لي دائمًا "البرنامج خرج مع 01". سيكون من الرائع أن يوضح لي البعض كيفية تصحيح هذا.
(gdb) break _start
Note: breakpoint -10 also set at pc 0x4000b0.
Breakpoint 8 at 0x4000b0
(gdb) start
Function "main" not defined.
Make breakpoint pending on future shared library load? (y or [n]) y
Temporary breakpoint 9 (main) pending.
Starting program: /home/claws/helloworld
Program exited with code 01.
(gdb) info breakpoints
Num Type Disp Enb Address What
8 breakpoint keep y 0x00000000004000b0 <_start>
9 breakpoint del y <PENDING> main
حاولت الجري strace
. هذا هو ناتجه:
execve("./helloworld", ["./helloworld"], [/* 39 vars */]) = 0
write(0, NULL, 12 <unfinished ... exit status 1>
- اشرح معلمات
write(0, NULL, 12)
استدعاء النظام في إخراج Strace؟ - ماذا بالضبط يحدث؟ اريد ان اعرف السبب بالضبط الخروج مع exitstatus = 1؟
- هل يمكن لشخص ما أن يريني كيفية تصحيح هذا البرنامج باستخدام GDB؟
- لماذا قاموا بتغيير أرقام مكالمات النظام؟
- يرجى تغيير هذا البرنامج بشكل مناسب حتى يمكن تشغيله بشكل صحيح على هذا الجهاز.
تعديل:
بعد قراءة إجابة بول آر. راجعت ملفاتي
claws@claws-desktop:~$ file ./hello.o
./hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
claws@claws-desktop:~$ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
وأنا أتفق معه على أن هذه يجب أن تكون قزم 32 بت قابلة للنقل وقابلة للتنفيذ. لكن هذا لا يجيب على أسئلتي. كل أسئلتي لا تزال أسئلة. ما الذي يحدث بالضبط في هذه الحالة؟ هل يمكن لأحد أن يجيب على أسئلتي وتقديم نسخة x86-64 من هذا الرمز؟
المحلول
تذكر أن كل شيء افتراضيًا على نظام التشغيل 64 بت يميل إلى تولي 64 بت. تحتاج إلى التأكد من أنك (أ) باستخدام الإصدارات 32 بت من #includes عند الاقتضاء (ب) الارتباط بمكتبات 32 بت و (ج) بناء 32 بت قابلة للتنفيذ. من المحتمل أن يساعدك ذلك إذا عرضت محتويات Makefile إذا كان لديك واحدة ، وإلا فإن الأوامر التي تستخدمها لبناء هذا المثال.
fwiw لقد غيرت رمزك قليلاً (_start -> Main):
#include <asm/unistd.h>
#include <syscall.h>
#define STDOUT 1
.data
hellostr:
.ascii "hello wolrd\n" ;
helloend:
.text
.globl main
main:
movl $(SYS_write) , %eax //ssize_t write(int fd, const void *buf, size_t count);
movl $(STDOUT) , %ebx
movl $hellostr , %ecx
movl $(helloend-hellostr) , %edx
int $0x80
movl $(SYS_exit), %eax //void _exit(int status);
xorl %ebx, %ebx
int $0x80
ret
وبناءها مثل هذا:
$ gcc -Wall test.S -m32 -o test
verfied أن لدينا 32 بت قابلة للتنفيذ:
$ file test
test: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), for GNU/Linux 2.6.4, dynamically linked (uses shared libs), not stripped
ويبدو أنه يعمل بشكل جيد:
$ ./test
hello wolrd
نصائح أخرى
كما لاحظ بول ، إذا كنت ترغب في إنشاء ثنائيات 32 بت على نظام 64 بت ، فأنت بحاجة إلى استخدام علامة -M32 ، والتي قد لا تكون متوفرة افتراضيًا على التثبيت (بعض توزيعات Linux 64 بت لا قم بتضمين دعم 32 بت/Linker/LIB بشكل افتراضي).
من ناحية أخرى ، يمكنك بدلاً من ذلك إنشاء الكود الخاص بك على أنه 64 بت ، وفي هذه الحالة تحتاج إلى استخدام اتفاقيات الاتصال 64 بت. في هذه الحالة ، يذهب رقم استدعاء النظام في ٪ rax ، وتذهب الوسائط في ٪ RDI و ٪ RSI و ٪ RDX
تعديل
أفضل مكان وجدته لهذا www.x86-64.org, ، على وجه التحديد ABI.PDF
يمكن لوحدة المعالجة المركزية 64 بت تشغيل رمز 32 بت ، ولكن يتعين عليهم استخدام وضع خاص للقيام بذلك. كل هذه الإرشادات صالحة في وضع 64 بت ، لذلك لا شيء يمنعك من بناء قابلة للتنفيذ 64 بت.
رمزك يبني ويعمل بشكل صحيح مع gcc -m32 -nostdlib hello.S
. ذلك بسبب -m32
تعريف __i386
, ، لذا /usr/include/asm/unistd.h
يشمل <asm/unistd_32.h>
, التي لديها الثوابت الصحيحة ل int $0x80
أبي.
أنظر أيضا تجميع ثنائيات 32 بت على نظام 64 بت (GNU أدوات) لمعرفة المزيد عن _start
ضد. main
مع/بدون libc و static مقابل Dynamic Executables.
$ file a.out
a.out: ELF 32-bit LSB executable, Intel 80386, version 1 (SYSV), statically linked, BuildID[sha1]=973fd6a0b7fa15b2d95420c7a96e454641c31b24, not stripped
$ strace ./a.out > /dev/null
execve("./a.out", ["./a.out"], 0x7ffd43582110 /* 64 vars */) = 0
strace: [ Process PID=2773 runs in 32 bit mode. ]
write(1, "hello wolrd\n", 12) = 12
exit(0) = ?
+++ exited with 0 +++
من الناحية الفنية ، إذا كنت قد استخدمت أرقام المكالمات المناسبة ، فسيحدث رمزك للعمل من وضع 64 بت أيضًا: ماذا يحدث إذا كنت تستخدم 32 بت 0x80 Linux ABI في رمز 64 بت؟ لكن int 0x80
لا ينصح به في رمز 64 بت. (في الواقع ، لا ينصح به أبدًا. من أجل الكفاءة ، يجب أن يتصل رمز 32 بت من خلال صفحة VDSO المصدرة من kernel حتى تتمكن من استخدامها sysenter
لمكالمات النظام السريع على وحدات المعالجة المركزية التي تدعمها).
لكن هذا لا يجيب على أسئلتي. ماذا بالضبط هو يحدث في هذه الحالة؟
سؤال جيد.
على Linux ، int $0x80
مع eax=1
هو sys_exit(ebx)
, ، بغض النظر عن الوضع الذي كانت عملية الاتصال فيه. يتوفر ABI 32 بت في وضع 64 بت (ما لم يتم تجميع نواةك دون دعم I386 ABI) ، ولكن لا تستخدمه. حالة الخروج الخاصة بك من movl $(STDOUT), %ebx
.
(راجع للشغل ، هناك أ STDOUT_FILENO
الماكرو المحدد في unistd.h
, ، لكن لا يمكنك ذلك #include <unistd.h>
من .S
لأنه يحتوي أيضًا على نماذج C التي ليست صالحة لـ ASM Syntax.)
لاحظ أن __NR_exit
من عند unistd_32.h
و __NR_write
من عند unistd_64.h
كلاهما 1
, ، بحيث أول int $0x80
يخرج عمليتك. أنت تستخدم أرقام مكالمات النظام الخاطئة لـ ABI التي تستدعيها.
strace
هو فك تشفيره بشكل غير صحيح, ، كما لو كنت قد استدعت syscall
(لأن هذه هي عملية ABI A 64 بت من المتوقع استخدامها). ما هي اتفاقيات الاتصال لمكالمات نظام UNIX & Linux على x86-64
eax=1
/ syscall
يعني write(rd=edi, buf=rsi, len=rdx)
, ، وهذه هي الطريقة strace
هو فك تشفير الخاص بك بشكل غير صحيح int $0x80
.
rdi
و rsi
نكون 0
(الملقب ب NULL
) عند الدخول إلى _start
, ، ويقوم الرمز الخاص بك بتعيين rdx=12
مع movl $(helloend-hellostr) , %edx
.
يقوم Linux بتهيئة السجلات إلى الصفر في عملية جديدة بعد execve. (يقول ABI غير محدد ، يختار Linux صفر لتجنب تسرب المعلومات). في الخاص بك المرتبطة بشكل ثابت ، تنفذ ، _start
هو أول رمز فضاء المستخدم الذي يتم تشغيله. (في تنفيذ ديناميكي ، يعمل الرابط الديناميكي من قبل _start
, ، ويترك القمامة في السجلات).
انظر أيضا x86 علامة wiki لمزيد من روابط ASM.