Запуск 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-битным и 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
Я согласен с ним, что это должен быть ELF 32-битный перемещаемый и исполняемый файл. Но это не отвечает на мои мои вопросы. Все мои вопросы еще вопросы. Что именно происходит в этом случае? Может кто-нибудь ответить на мои вопросы и предоставить версию этого кода для x86-64?
3 ответа
Помните, что по умолчанию все в 64-битной ОС имеет тенденцию принимать 64-битную. Вы должны убедиться, что вы (а) используете 32-битные версии вашего #include, где это необходимо, (б) связываете с 32-битными библиотеками и (в) создаете 32-битный исполняемый файл. Вероятно, было бы полезно, если бы вы показали содержимое вашего make-файла, если он у вас есть, или команды, которые вы используете для построения этого примера.
Я немного изменил ваш код (_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
Как отметил Пол, если вы хотите собрать 32-битные двоичные файлы в 64-битной системе, вам нужно использовать флаг -m32, который может быть недоступен по умолчанию в вашей установке (некоторые 64-битные дистрибутивы Linux не включает поддержку 32-битного компилятора / компоновщика /lib по умолчанию).
С другой стороны, вы могли бы вместо этого построить свой код как 64-битный, в этом случае вам нужно использовать 64-битные соглашения о вызовах. В этом случае номер системного вызова указывается в%rax, а аргументы - в%rdi, %rsi и%rdx.
редактировать
Лучшее место, которое я нашел для этого - http://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
ABI.
См. Также Сборка 32-битных двоичных файлов в 64-битной системе (набор инструментов GNU) для получения дополнительной информации о _start
против 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
,
(Кстати, есть 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
(ака NULL
) при въезде в _start
и ваши кодовые наборы rdx=12
с movl $(helloend-hellostr) , %edx
,
Linux инициализирует регистры в ноль в новом процессе после execve. (ABI говорит, что не определено, Linux выбирает ноль, чтобы избежать утечки информации). В вашем статически связанном исполняемом файле _start
это первый код пользовательского пространства, который запускается. (В динамическом исполняемом файле динамический компоновщик запускается до _start
и оставляет мусор в регистрах).
Смотрите также вики-тег x86 для получения дополнительных ссылок на asm.