Почему эти программы `const int main = 0xc3` (или другое число) возвращают 252 на OS X? - PullRequest
0 голосов
/ 24 января 2019

Я слышал о «самой короткой программе на C, которая приводит к недопустимой инструкции»: const main=6; для x86-64 более на codegolf.SE , и мне стало любопытно, что произойдет, если я введу разные числа есть.

Теперь я думаю, что это связано с тем, что является или не является действительной инструкцией x86-64 (durr), но, в частности, я хотел бы знать, что означают различные результаты.

  • const main=0 - 2 дают ошибку шины.
  • const main=3 дает ошибку по ошибке.
  • 6 и 7 дают недопустимые инструкции.

Я получаю различные ошибки шины, ошибки и недопустимые инструкции до const main=194, который вообще не давал мне прерывания (по крайней мере, не дошло до моего скрипта на python, который генерировал эти маленькие программы).

Есть несколько других чисел, которые также не приводят к исключениям / прерываниям и, следовательно, к сигналам Unix. Я проверил код возврата пары, и код возврата был 252. Я не знаю, почему или что это значит или как оно туда попало.

204 достал мне "следовую ловушку". Это 0xcc, которое я знаю как int3 прерывание - это весело! (241 / 0xf1 тоже получает это)


В любом случае, он продолжает работать, и это, очевидно, в основном ошибки шины и ошибки, а также несколько недопустимых инструкций здесь и там, и случайные ... делает все, что делает, а затем возвращает с 252 ...

Я гуглил некоторые коды операций, но я действительно не знаю, что я делаю или где искать, чтобы быть честным. Я даже не посмотрел на все свои результаты, а просто прокручивал. Я понимаю, что segfault - это недопустимый доступ к действительной памяти, а ошибка шины - это доступ к недействительной памяти, и я планирую посмотреть шаблоны чисел и выяснить, где это происходит и почему. Но 252 вещь немного озадачена.

#!/usr/bin/env python3
import os
import subprocess
import time
import signal

os.mkdir("testc")
try:
    os.chdir("testc")
except:
    print("Could not change directory, exiting.")

for i in range(0, 65536):
    filename = "test" + str(i) + ".c"
    f = open(filename, "w")
    f.write("const main=" + str(i) + ";")
    f.close()
    outname = "test" + str(i)
    subprocess.Popen(["gcc", filename, "-o",  outname], stdout=subprocess.PIPE, stderr=subprocess.PIPE)
    time.sleep(1)
    err = subprocess.Popen("./" + outname, shell=True)
    result = None
    while result is None:
        result = err.poll()
    r = result
    if result == -11:
        r = "segfault"
    if result == -10:
        r = "bus error"
    if result == -4:
        r = "illegal instruction"
    if result == -5:
        print = "trap"
    print("const main=" + str(hex(i)) + " : " + r)

Это создает программу на C в testc/test20.c как

const int main=20;

Затем компилирует это с gcc и запускает его. (И спит в течение 1 секунды, прежде чем набрать следующий номер.)

Не было никаких ожиданий. Я просто хотел посмотреть, что случилось.

1 Ответ

0 голосов
/ 24 января 2019

int main = 194 равно c2 00 00 00, которое декодируется как ret 0

Независимо от того, что называется main, должно быть оставлено 252 в младшем байте RAX . (Соглашение о вызовах гласит, что RAX является регистром возвращаемых значений, но это не регистр передачи аргументов, поэтому при входе в функцию он хранит любой мусор tmp, для которого его вызывающая сторона использовала его.)

В нижней части ответа приведена теория о том, почему вы получаете SIGBUS для 2, а SIGSEGV для 3: Я думаю, что RAX является действительным указателем при входе в main (случайно из-за того, что имел динамический компоновщик там), 03 00 add eax, [rax] уничтожает его, но 02 00 add al, [rax] нет, и затем выполнение либо дает сбой на 00 00 add [rax], al из следующих 2 байтов main, либо выполняет инструкцию 00 00, а затем падает до конца страница.

Обновление от @ MichaelPetch : RAX равно , указывающему на main (в сегменте TEXT только для чтения), и сохраняет на страницах только для чтения также SIGBUS. Таким образом, 00 00 add [rax], al будет SIGBUS по этой причине, если RAX все еще указывает туда.

(Остерегайтесь, что в этом ответе есть неправильные предположения, и он не переписывался полностью каждый раз, когда я получал новую информацию от @SWilliams или @MichaelPetch. Пуля указывает на то, какие типы #PF вызывают тот или иной сигнал, и Я попытался, по крайней мере, добавить исправление после того, что было не совсем точным. Я думаю, что в неправильных теориях есть какая-то ценность, как иллюстрация других вещей, которые могли бы случиться, поэтому я Я оставляю все это здесь.)


Ваша программа на Python не работает на моем Linux-компьютере, как только она достигает c2 00 00 00 ret imm16, первой, которая успешно возвращается. (В Linux секция .rodata заканчивается после .text в TEXT-сегменте , поэтому main не в чем попадать.)

...
const main=0xc0 : segfault
const main=0xc1 : segfault
Traceback (most recent call last):
  File "./opcode-test.py", line 34, in <module>
    print("const main=" + str(hex(i)) + " : " + r)
TypeError: must be str, not int

Разве Python не имеет эквивалента strsignal(3) для отображения сигналов в стандартные текстовые строки, такие как "Недопустимая инструкция"? (Как strerror, но для кодов сигналов вместо значений errno?)


Большинство команд x86 имеют длину в несколько байтов . x86 имеет младший порядок байтов, поэтому вы в основном смотрите на
?? 00 00 00 90 90 90 ... или для больших целых чисел ?? ?? 00 00 90 90 90 90 ..., при условии, что ваш компоновщик заполняет байты между функциями с 0x90 nop, как это делает GNU ld в Linux.

Эти байтовые последовательности могут декодироваться в одну или несколько действительных инструкций до того, как вы нажмете NOP, и перейдете к любой функции CRT, которую компоновщик ставит после main. Если вы попали туда без сбоев и без смещения указателя стека, вы ввели функцию с действительным адресом возврата в стеке (вызывающий main, еще одна функция CRT), точно так же, как если бы основной хвост вызвал ее.

Предположительно, эта функция возвращает 252 (или более широкое значение, младший байт которого равен 252). Возврат из main приводит к чистому завершению процесса, делая системный вызов выхода с возвращаемым значением main.

Этот провальный обратный вызов похож на основной, заканчивающийся return next_function(argc, argv);.


Исправление (без переписывания всего ответа, извините)

Поскольку main=194 является первым, который сработал , я думаю, что вы на самом деле не получаете провал, вероятно, только C2 ret imm16 и C3 ret ведут к чистому выходу. А для c2 за ним должны следовать 2 00 байтов, иначе он сломает стек для вызывающей стороны main.

Или те инструкции с префиксом, которые ничего не делают, или безобидная однобайтовая инструкция . например 90 nop / c3 ret или 90 nop / c2 00 00 ret 0. Или 91 xchg eax, ecx и т. Д. Может фактически дать вам другое возвращаемое значение, заменяя EAX другим регистром. (x86 выделяет коды операций 90 .. 97 для xchg-with-EAX, потому что в оригинальном 8086 AX был более «особенным», без инструкций, таких как movsx, для расширения знака в другие регистры. И без 2 операндов imul.

Другие безобидные однобайтовые инструкции включают 99 cdq и 98 cwde, но не push или pop (потому что изменение RSP не позволит указать адрес возврата).Некоторыми однобайтовыми инструкциями установки / сброса флага являются f9 stc, fd std, но не fb sti (это привилегировано, в отличие от флага переноса и флага направления).

Префиксы безвредны 0x40..4f REX префиксы, 0xf2 / f3 REP, and 0x66 and 0x67` размер операнда и размер адреса.Также любые префиксы переопределения сегментов также могут быть безвредными.

Я только что протестировал main=0xc366 и main=0xc367, и да, они оба выходят корректно.GDB декодирует 66 c3 как retw (префикс размера операнда) и 67 c3 как addr32 ret (префикс размера адреса), но оба по-прежнему извлекают 64-битный адрес возврата и также не усекают указатель стека.(Я вынул -no-pie, который использовал, поэтому RIP был вне младших 32 бит вместе с RSP).


Обратите внимание, что 00 - это код операции для add [r/m8], r8, поэтому00 00 декодируется как add [rax], al.

Чтобы пройти эти 00 байты и добраться до "nop sled", которые компоновщик вставляет как заполнение, вам нужен код операции(и байт modrm, если код операции использует единицу) для кодирования начала более длинной инструкции , например 0xb8 mov eax, imm32 длиной 5 байтов, которая использует следующие 4 байта после 0xb8.На самом деле для каждого регистра существуют краткие кодировки mov-немедленного, поэтому 0xb8 + 0..7 поможет вам преодолеть пробел.За исключением mov esp, imm32, который приведет к сбою при переходе к следующей функции, потому что он наступил на указатель стека.

Одним из первых является 05, краткая форма (без модема) код операции для add eax, imm32.Большинство оригинальных инструкций ALU-8086 имеют специальную краткую форму AX, imm16 / EAX, imm32 вместо формы op r/m32, imm32 или imm8, которая использует байт ModRM для кодирования операнда-адресата.(И биты поля /r в ModRM как дополнительные биты кода операции.)

См. Советы по игре в гольф в машинном коде x86 / x64 для получения дополнительной информации о AL / EAX /Краткие кодировки RAX и однобайтовые инструкции.

Для ручного декодирования машинного кода x86 см. Руководства Intel, особенно руководство vol.2, в котором подробно описываются форматы кодирования инструкций, и имеется таблица кодов операций наконец.(См. Ссылки в теге x86 вики ). Для получения только карты кодов операций см. http://ref.x86asm.net/coder64.html.


Используйте дизассемблер или отладчик, чтобы увидеть, что находится в ваших исполняемых файлах

Но на самом деле используйте дизассемблер, например objdump -drwC -Mintel.Или llvm-objdump.Найдите main в выводе и посмотрите, что вы получите.(Или используйте GDB, потому что метки в середине инструкции отбрасывают дизассемблер.)

Используйте objdump -rwC -Mintel -D -j .rodata -j .text testc/test194, чтобы получить вывод, подобный этому, разбирая секции .text и .rodata какcode:

testc/test194:     file format elf64-x86-64


Disassembly of section .text:

0000000000400540 <__libc_csu_init>:
  400540:       41 57                   push   r15
  400542:       49 89 d7                mov    r15,rdx
  ...
  4005a4:       c3                      ret    
  4005a5:       90                      nop
  4005a6:       66 2e 0f 1f 84 00 00 00 00 00   nop    WORD PTR cs:[rax+rax*1+0x0]

00000000004005b0 <__libc_csu_fini>:
  4005b0:       c3                      ret    

Disassembly of section .rodata:

00000000004005c0 <_IO_stdin_used>:     ;;;; This is actually data!
  4005c0:       01 00                   add    DWORD PTR [rax],eax
  4005c2:       02 00                   add    al,BYTE PTR [rax]

00000000004005c4 <main>:
  4005c4:       c2 00 00                ret    0x0
        ...             ; objdump elided the last 0, not me.  It literally put ...

(Я изменил ваш скрипт на python, добавив опцию -no-pie gcc, поэтому в моей разборке есть абсолютные адреса, а не просто маленькие адреса относительно начала файла = 0.Я задавался вопросом, могло ли это поместить main туда, где оно могло бы провалиться, но это не так.)

Обратите внимание, что между .text и .rodata есть только небольшой разрыв.Они являются частью одного и того же сегмента ELF (в заголовках программ ELF, на которые смотрит загрузчик программы ОС), поэтому они являются частью одного и того же отображения, между ними нет отображенных страниц. Еслинам повезло, промежуточные байты даже заполнены 0x90 nop вместо 00.На самом деле, что-то заполнило разрыв между __libc_csu_init и __libc_csu_fini длинными NOP.Может быть, это было от ассемблера, если они были в одном исходном файле.

main, конечно, в .rodata, потому что вы объявили его в C как глобальный доступ только для чтения (статическое хранилище), например const int main = 6;.Если вы использовали const int main __attribute__((section(".text"))) = 123, вы можете получить main в обычном разделе .text.В моей системе это заканчивается прямо перед __libc_csu_init.

Но этикетки прерывают разборку;дизассемблер считает, что это не так, и возобновляет декодирование с метки.Таким образом, в GDB на testc/test5set disassembly-flavor intel и layout reg, а затем с помощью команды start для остановки в начале main) я получу

   |0x40053c <main>                 add    eax,0x41000000                                                                                                 │
   │0x400541 <__libc_csu_init+1>    push   rdi                                                                                                            │
   │0x400542 <__libc_csu_init+2>    mov    r15,rdx         

Но изobjdump -drwC -Mintel (разборка только раздела .text является значением по умолчанию для -d, и я использовал атрибут GNU C, чтобы поместить туда main, чтобы моя программа могла работать так же, как ваша), я получаю:

000000000040053c <main>:
  40053c:       05 00 00 00                                         ....

0000000000400540 <__libc_csu_init>:
  400540:       41 57                   push   r15
  400542:       49 89 d7                mov    r15,rdx

Обратите внимание, что .... в той же строке, что и 05 00 00 00, указывает, что декодирование не дошло до конца инструкции.

И поскольку main не выравнивается по16 здесь, это прямо против начала __libc_csu_init.Таким образом, add eax, imm32 использует префикс REX.W (41) из push r15, что делает его декодируемым как push rdi, если достигается путем падения с основного вместо вызова метки __libc_csu_init.


Вышеуказанный вывод был из Linux.Ваша система OS X будет другой

OS X помещает большую часть кода запуска CRT в libc, а не статически связывается с исполняемым файлом с помощью main.

Или, может быть, для этого ничего нетваш главный провал в

Если бы было, main=5 сработало бы, но вы говорите, что первый результат без сбоев был с main=194, который является действительным ret.

Если ничего не возвращалось до c3 ret или c2 00 00 ret 0, то, вероятно, нечего попадать после main, или разрыв не заполняется повторными 90 nop, чтобы сформировать "nop sled", который будет выполняться нормально, еслидекодирование начинается где-нибудь в середине этого.(например, после того, как более ранняя инструкция потребляет завершающие 0 байтов в конце dword int main и некоторые байты заполнения.)


Я понимаю, что ошибка сегмента невернадоступ к действительной памяти и ошибка шины - это доступ к недействительной памяти

Нет, это упрощенное описание обратное.Обычно вы получаете segfault за попытку доступа к не отображенной странице во всех Unix-системах.Но вы получаете ошибку шины для некоторых видов недопустимого доступа (даже для действительных адресов).

Solaris в SPARC выдает ошибку шины для неправильно выровненных загрузок / сохранений слов в действительную память.

Вкл.x86-64 Linux, вы получаете SIGBUS только за действительно странные вещи.См. Отладка SIGBUS на x86 Linux .Указатель неканонического стека, приводящий к исключению #SS, читающий после конца mmap ed файла, который был усечен.Также, если вы включаете проверку выравнивания x86 (флаг AC), но никто этого не делает, потому что библиотечные функции, такие как memcpy, используют невыровненные загрузки / хранилища, а код компилятора предполагает, что невыровненные целочисленные загрузки / хранилища безопасны.

IDK чтоаппаратные исключения * BSD сопоставляется с SIGBUS, но я бы предположил, что обычный выход за пределы допустимого диапазона, например разыменование NULL-указателя, будет SIGSEGV.Это довольно стандартно.


@ MichaelPetch говорит в комментариях, что на OS X

  • #PF (исключение аппаратного сбоя страницы) из выборки кодав случае, если ядро ​​при доставке SIGBUS
  • #PF из загрузки / хранения данных на не отображенную страницу приводит к SIGSEGV.
  • #PF из хранилища на страницу только для чтения приводит к SIGBUS,(И это - это то, что происходит после 02 00 add al, [rax], в 00 00 add [rax], al, который формирует 2-й байт main. Остальная часть этого ответа не принимает это во внимание.)

(Конечно, это после проверки того, произошел ли сбой страницы из-за разницы между таблицей аппаратного обеспечения и картой памяти логического процесса, например, из-за отложенного отображения, копирования при записи или страниц, выгруженных на диск.)

Так что, если ваш int main приземляется в самом конце не отображенной страницы, 05 add eax,imm32 будет читать один дополнительный байт после конца удерживающего меча int main (.long 5 в синтаксисе GAS asm). Это перейдет на следующую страницу и к SIGBUS. (Ваш последний комментарий указывает, что это делает SIGBUS.)

Теория того, что происходит с первыми несколькими значениями:

Вы сообщаете:

  • ошибка шины для main = 02 00 add al, [rax] / `00 00 add [rax], al
  • , но для основного = segfault * 03 00 add eax, [rax] / 00 00 add [rax], al.

Мы знаем, что младший байт RAX равен 252, поэтому, если RAX содержит действительное значение указателя, он выровнен на 4 байта. Таким образом, если загрузка байта из [rax] работает, то и загрузка dword.

Так что, вероятно, источник памяти add следует за , и изменяя AL, младший байт RAX (размер операнда байта), вероятно, все еще оставляет RAX действительным указателем. ** Тогда, если остальные страница, содержащая main, заполнена 00 00 add [rax], al инструкциями (или только одной внутри самой основной), они будут успешными (без дальнейшей модификации RAX), пока выполнение не упадет на неотображенную страницу, пока RAX все еще действительный указатель после выполнения любого main, декодированного в.

На самом деле адресат памяти add сам отказывает и вызывает SIGBUS.

03 00 add eax, [rax] записывает EAX и, таким образом, усекает RAX до 32-битного. (запись 32-битного регистра неявно расширяет ноль в полный 64-битный регистр, в отличие от записи младшего 8 или 16 парциального регистры.) Это определенно дает вам недопустимый указатель , потому что OS X отображает статический код / ​​данные за пределы младших 32 бит виртуального адресного пространства.
Так что следующее 00 00 add [rax], al определенно приведет к ошибке в попытке написать адрес за пределами допустимого диапазона, в результате чего #PF вызовет SIGSEGV.

Вероятно, всего лишь единица 00 00 из последних двух байтов main перед концом страницы. В противном случае 05 add eax, imm32 приведет к отключению от усечения RAX, а затем работает 00 00 add [rax], al. Для этого в SIGBUS он должен извлечь код на несопоставленную страницу без декодирования каких-либо инструкций доступа к памяти после этого.

Конечно, есть и другие правдоподобные объяснения того, что вы видите, но я думаю, что это объясняет все ваши наблюдения; без большего количества данных мы не можем опровергнуть это. Очевидно, что проще всего было бы запустить GDB или любой другой отладчик и просто start / si и посмотреть, что произойдет.

...