Запуск 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 и оставляет мусор в регистрах).

См. также tag wiki для дополнительных ссылок asm.