Запуск 32-разрядного ассемблерного кода на 64-разрядном процессоре Linux и 64 бит: объясните аномалию
У меня интересная проблема. Я забыл, что использую 64-битную машину и ОС и написал 32-битный ассемблерный код. Я не знаю, как писать 64-битный код.
Это 32-разрядный ассемблерный код x86 для Gnu Assembler (синтаксис AT & T) в 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-битными процессорами. Таким образом, это также не будет проблемой. Проблема возникает из-за различий в системных вызовах и механизмах вызова в 64-битной ОС и 32-разрядной ОС. Я не знаю, почему, но они изменили номера системных вызовов между 32-битным Linux и 64-битным Linux.
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?
- Почему они меняли номера системных вызовов?
- Не забудьте изменить эту программу соответствующим образом, чтобы она могла корректно работать на этом компьютере.
EDIT:
Прочитав ответ Пола Р. Я проверил файлы
[email protected]:~$ file ./hello.o
./hello.o: ELF 64-bit LSB relocatable, x86-64, version 1 (SYSV), not stripped
[email protected]:~$ file ./hello
./hello: ELF 64-bit LSB executable, x86-64, version 1 (SYSV), statically linked, not stripped
Я согласен с ним в том, что это должно быть 32-битное перемещение и исполняемый файл ELF. Но это не отвечает на мои вопросы. Все мои вопросы все еще задают вопросы. Что именно происходит в этом случае? Может ли кто-нибудь ответить на мои вопросы и предоставить версию этого кода x86-64?
Ответы
Ответ 1
Помните, что все по умолчанию на 64-битной ОС имеет тенденцию предполагать 64-разрядную. Вы должны убедиться, что вы (а) используете 32-битные версии вашего #includes, где это необходимо (b), связывающие с 32-битными библиотеками, и (c) создание 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
подтвердил, что у нас есть 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
Ответ 2
Как отметил Пол, если вы хотите создать 32-разрядные двоичные файлы в 64-разрядной системе, вам нужно использовать флаг -m32, который по умолчанию может быть недоступен при установке (некоторые 64-разрядные дистрибутивы Linux не включают 32-разрядную поддержку компилятора/компоновщика/lib по умолчанию).
С другой стороны, вы могли бы вместо этого создать свой код как 64-битный, и в этом случае вам нужно использовать соглашения с 64-битными вызовами. В этом случае номер системного вызова идет в% rax, а аргументы идут в% rdi,% rsi и% rdx
Edit
Лучшее место, которое я нашел для этого, - www.x86-64.org, в частности abi.pdf
Ответ 3
64-разрядные ЦП могут запускать 32-разрядный код, но для этого им нужен специальный режим. Эти инструкции действительны в 64-битном режиме, поэтому ничто не мешало вам создать 64-битный исполняемый файл.
Ваш код строит и работает правильно с помощью gcc -m32 -nostdlib hello.S
. Это потому, что -m32
определяет __i386
, поэтому /usr/include/asm/unistd.h
включает <asm/unistd_32.h>
, который имеет правильные константы для int $0x80
ABI.
См. также Сборка 32-битных двоичных файлов в 64-битной системе (инструментальная цепочка GNU) для получения дополнительной информации о _start
vs. main
с/без libc и статические и динамические исполняемые файлы.
$ 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-битный int 0x80 Linux ABI в 64-битном коде? Но int 0x80
не рекомендуется в 64-битном коде. (На самом деле это никогда не рекомендуется. Для эффективности 32-разрядный код должен вызывать через экспортированную ядром страницу VDSO, чтобы она могла использовать sysenter
для быстрых системных вызовов на поддерживающих ее процессорах).
Но это не отвечает на мои вопросы. Что именно происходит в этом случае?
Хороший вопрос.
В Linux int $0x80
с eax=1
есть sys_exit(ebx)
, независимо от того, в каком режиме находился вызывающий процесс. 32-разрядный ABI доступен в 64-битном режиме (если только ваш ядро было скомпилировано без поддержки i386 ABI), но не используйте его. Ваш статус выхода от movl $(STDOUT), %ebx
.
(BTW, там макрос STDOUT_FILENO
, определенный в unistd.h
, но вы не можете #include <unistd.h>
из .S
, потому что он также содержит прототипы C, которые недопустимы как синтаксис asm.)
Обратите внимание, что __NR_exit
из unistd_32.h
и __NR_write
из unistd_64.h
являются 1
, поэтому ваш первый int $0x80
завершает ваш процесс. Вы используете неправильные номера системных вызовов для вызываемого ABI.
strace
неправильно декодирует его, как если бы вы вызывали syscall
(потому что предполагается, что ABI будет использовать 64-разрядный процесс). Каковы соглашения о вызовах для системных вызовов UNIX и Linux на x86-64
eax=1
/syscall
означает write(rd=edi, buf=rsi, len=rdx)
, и именно так strace
неправильно декодирует ваш int $0x80
.
rdi
и rsi
являются 0
(aka NULL
) при входе в _start
, а ваш код устанавливает rdx=12
с помощью movl $(helloend-hellostr) , %edx
.
Linux инициализирует регистры до нуля в новом процессе после execve. (ABI говорит undefined, Linux выбирает ноль, чтобы избежать утечки информации). В вашем статически связанном исполняемом файле _start
- это первый код пользовательского пространства, который выполняется. (В динамическом исполняемом файле динамический компоновщик работает до _start
и оставляет мусор в регистрах).
См. также x86 tag wiki для дополнительных ссылок asm.