Запуск 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>
  1. Объясните параметры write(0, NULL, 12) системный вызов в выводе strace?
  2. Что именно происходит? Я хочу знать причину, почему именно его выход с exitstatus=1?
  3. Кто-нибудь может показать мне, как отладить эту программу с помощью GDB?
  4. Почему они изменили номера системных вызовов?
  5. Пожалуйста, измените эту программу соответствующим образом, чтобы она могла правильно работать на этом компьютере.

РЕДАКТИРОВАТЬ:

Прочитав ответ Пола Р. Я проверил свои файлы

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.

Другие вопросы по тегам