Запуск 32-битного ассемблерного кода на 64-битном Linux и 64-битном процессоре: объясните аномалию - PullRequest
14 голосов
/ 23 марта 2010

У меня интересная проблема. Я забыл, что использую 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. Пожалуйста, измените эту программу соответствующим образом, чтобы она могла правильно работать на этом компьютере.

EDIT:

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

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-битный ELF-файл, который можно перемещать и выполнять. Но это не отвечает на мои мои вопросы. Все мои вопросы еще вопросы. Что именно происходит в этом случае? Может кто-нибудь ответить на мои вопросы и предоставить версию этого кода для x86-64?

Ответы [ 3 ]

8 голосов
/ 23 марта 2010

Помните, что по умолчанию все в 64-битной ОС имеет тенденцию принимать 64-битную. Вы должны убедиться, что вы (а) используете 32-битные версии вашего #include, где это необходимо, (б) связываете с 32-битными библиотеками и (в) создаете 32-битный исполняемый файл. Вероятно, было бы полезно, если бы вы показали содержимое вашего make-файла, если он у вас есть, или команды, которые вы используете для построения этого примера.

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
6 голосов
/ 24 марта 2010

Как отметил Пол, если вы хотите собрать 32-битные двоичные файлы в 64-битной системе, вам необходимо использовать флаг -m32, который может быть недоступен по умолчанию в вашей установке (некоторые 64-битные дистрибутивы Linux по умолчанию не включать поддержку 32-битного компилятора / компоновщика / lib).

С другой стороны, вы могли бы вместо этого построить свой код как 64-битный, и в этом случае вам нужно использовать 64-битные соглашения о вызовах. В этом случае номер системного вызова указывается в% rax, а аргументы - в% rdi,% rsi и% rdx

Редактировать

Лучшее место, которое я нашел для этого, - www.x86-64.org , в частности abi.pdf

1 голос
/ 03 сентября 2017

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

См. Также вики-тег для дополнительных ссылок asm.

...