Ошибка сегментации 11, связывающая 32-битный ассемблер os x

ОБНОВЛЕНИЕ: Конечно же, это была ошибка в последней версии nasm. Я «понизил» и после исправления моего кода, как показано в ответе, который я принял, все работает правильно. Всем спасибо!

У меня проблемы с тем, что должно быть очень простой программой на 32-битном ассемблере в OS X.

Во-первых, код:

section .data
hello   db  "Hello, world", 0x0a, 0x00

section .text
default rel

global _main
extern _printf, _exit

_main:
    sub esp, 12     ; 16-byte align stack
    push hello
    call _printf

    push 0
    call _exit

Он собирается и связывается, но когда я запускаю исполняемый файл, он падает с ошибкой сегментации: 11.

Командные строки для сборки и компоновки:

nasm -f macho32 hello32x.asm -o hello32x.o

Я знаю, что -o там не на 100 процентов нужно

Связывание:

ld -lc -arch i386 hello32x.o -o hello32x

Когда я запускаю его в lldb для отладки, все в порядке, пока он не войдет в вызов _printf, где произойдет сбой, как показано ниже:

  (lldb) s
  Process 1029 stopped
  * thread #1: tid = 0x97a4, 0x00001fac hello32x`main + 8, queue = 'com.apple.main-thread', stop reason = instruction step into
      frame #0: 0x00001fac hello32x`main + 8
  hello32x`main:
  ->  0x1fac <+8>:  calll  0xffffffff991e381e
      0x1fb1 <+13>: pushl  $0x0
      0x1fb3 <+15>: calll  0xffffffff991fec84
      0x1fb8:       addl   %eax, (%eax)
  (lldb) s
  Process 1029 stopped
  * thread #1: tid = 0x97a4, 0x991e381e libsystem_c.dylib`vfprintf + 49, queue = 'com.apple.main-thread', stop reason = instruction step into
      frame #0: 0x991e381e libsystem_c.dylib`vfprintf + 49
  libsystem_c.dylib`vfprintf:
  ->  0x991e381e <+49>: xchgb  %ah, -0x76f58008
      0x991e3824 <+55>: popl   %esp
      0x991e3825 <+56>: andb   $0x14, %al
      0x991e3827 <+58>: movl   0xc(%ebp), %ecx
  (lldb) s
  Process 1029 stopped
  * thread #1: tid = 0x97a4, 0x991e381e libsystem_c.dylib`vfprintf + 49, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=1, address=0x890a7ff8)
      frame #0: 0x991e381e libsystem_c.dylib`vfprintf + 49
  libsystem_c.dylib`vfprintf:
  ->  0x991e381e <+49>: xchgb  %ah, -0x76f58008
      0x991e3824 <+55>: popl   %esp
      0x991e3825 <+56>: andb   $0x14, %al
      0x991e3827 <+58>: movl   0xc(%ebp), %ecx

Как вы можете видеть внизу, он останавливается из-за ошибки плохого доступа.


person querist    schedule 12.03.2016    source источник
comment
Я почти уверен, что вы также получите как минимум предупреждение во время связывания. Во всяком случае, это похоже на обычный случай, когда библиотека C не инициализируется. Используйте gcc для ссылки.   -  person Jester    schedule 13.03.2016
comment
Привет @Jester. Да, я получаю сообщение об ошибке: ld: предупреждение: PIE отключен. Абсолютная адресация (возможно, -mdynamic-no-pic) не разрешена в PIE с подписью кода, но используется в _main из hello32x.o. Чтобы исправить это предупреждение, не компилируйте с -mdynamic-no-pic и не связывайте с -Wl,-no_pie, но я получаю ту же ошибку, если использую gcc для связывания.   -  person querist    schedule 13.03.2016
comment
Я нашел ошибку в своем коде - я должен был сделать только sub esp, 8, потому что занесение адреса строки в стек привело бы к 12, а затем заталкивание обратного адреса вызовом довело бы его до 16, что сделало бы 16-байтовое выравнивание стека, необходимое для OS X.   -  person querist    schedule 23.03.2016


Ответы (1)


Выравнивание 16-байтового стека

Одной из серьезных проблем с вашим кодом является выравнивание стека. 32-битный код OS/X требует выравнивания стека по 16 байтам в момент выполнения CALL. Соглашение о вызовах Apple IA-32 говорит следующее:

Соглашения о вызовах функций, используемые в среде IA-32, такие же, как и в Система V IA-32 ABI со следующими исключениями:

  • Различные правила возврата структур
  • Стек выравнивается по 16 байтам в точке вызова функции
  • Большие типы данных (более 4 байтов) сохраняются в своем естественном выравнивании.
  • Большинство операций с плавающей запятой выполняются с использованием модуля SSE вместо x87 FPU, за исключением случаев, когда используются длинные двойные значения. (Среда IA-32 по умолчанию использует 64-битную внутреннюю точность для x87 FPU.)

Вы вычитаете 12 из ESP, чтобы выровнять стек по 16-байтовой границе (4 байта для адреса возврата + 12 = 16). Проблема в том, что когда вы выполняете CALL функцию, стек ДОЛЖЕН быть выровнен по 16 байтам непосредственно перед самим CALL. К сожалению, вы вставляете 4 байта перед вызовом printf и exit. Это смещает стек на 4, тогда как он должен быть выровнен по 16 байтам. Вам придется переработать код с правильным выравниванием. Кроме того, вы должны очистить стек после того, как сделаете вызов. Если вы используете PUSH для помещения параметров в стек, вам нужно настроить ESP после вашего CALL, чтобы восстановить стек в его предыдущее состояние.

Одним из наивных способов (не моя рекомендация) исправить код было бы сделать это:

section .data
hello   db  "Hello, world", 0x0a, 0x00

section .text
default rel

global _main
extern _printf, _exit

_main:
    sub esp, 8     
    push hello     ; 4(return address)+ 8 + 4 = 16 bytes stack aligned
    call _printf
    add esp, 4     ; Remove arguments

    push 0         ; 4 + 8 + 4 = 16 byte alignment again
    call _exit     ; This will not return so no need to remove parameters after

Приведенный выше код работает, потому что мы можем воспользоваться тем фактом, что обе функции (exit и printf) требуют размещения в стеке ровно одного DWORD для параметров. 4 байта для адреса возврата main, 8 для корректировки стека, которую мы сделали, 4 для параметра DWORD = выравнивание по 16 байтам.


Лучший способ сделать это — вычислить объем стека, который вам потребуется для всех ваших локальных переменных на основе стека (в данном случае 0) в вашей функции main, плюс максимальное количество байтов, которое вам потребуется для любых параметров для вызовов функций. сделанный main, а затем убедитесь, что вы добавили достаточно байтов, чтобы значение делилось без остатка на 12. В нашем случае максимальное количество байтов, которое необходимо передать для любого вызова данной функции, составляет 4 байта. Затем мы прибавляем 8 к 4 (8+4=12), чтобы число делилось без остатка на 12. Затем мы вычитаем 12 из ESP в начале нашей функции.

Вместо использования PUSH для помещения параметров в стек теперь можно перемещать параметры непосредственно в стек в зарезервированное нами пространство. Поскольку мы не PUSH, стек не смещается. Поскольку мы не использовали PUSH, нам не нужно исправлять ESP после вызова нашей функции. Тогда код может выглядеть примерно так:

section .data
hello   db  "Hello, world", 0x0a, 0x00

section .text
default rel

global _main
extern _printf, _exit

_main:
    sub esp, 12           ; 16-byte align stack + room for parameters passed
                          ; to functions we call
    mov [esp],dword hello ; First parameter at esp+0
    call _printf

    mov [esp], dword 0    ; First parameter at esp+0
    call _exit

Если вы хотите передать несколько параметров, вы помещаете их в стек вручную, как мы сделали с одним параметром. Если бы мы хотели напечатать целое число 42 как часть нашего вызова printf, мы могли бы сделать это следующим образом:

section .data
hello   db  "Hello, world %d", 0x0a, 0x00

section .text
default rel

global _main
extern _printf, _exit

_main:
    sub esp, 12           ; 16-byte align stack + room for parameters passed
                          ; to functions we call

    mov [esp+4], dword 42 ; Second parameter at esp+4
    mov [esp],dword hello ; First parameter at esp+0
    call _printf

    mov [esp], dword 0    ; First parameter at esp+0
    call _exit

При запуске мы должны получить:

Привет, мир 42


16-байтовое выравнивание стека и кадр стека

Если вы хотите создать функцию с типичным фреймом стека, код в предыдущем разделе должен быть скорректирован. При входе в функцию в 32-битном приложении стек смещается на 4 байта, потому что адрес возврата был помещен в стек. Типичный пролог стекового фрейма выглядит так:

push ebp
mov  ebp, esp

Помещение EBP в стек после входа в вашу функцию по-прежнему приводит к смещению стека, но теперь оно смещено на 8 байтов (4 + 4).

Из-за этого код должен вычесть 8 из ESP, а не 12. Кроме того, при определении пространства, необходимого для хранения параметров, локальных переменных стека и байтов заполнения для выравнивания, размер выделения стека должен быть равномерным. делится на 8, а не на 12. Код со фреймом стека может выглядеть так:

section .data
hello   db  "Hello, world %d", 0x0a, 0x00

section .text
default rel

global _main
extern _printf, _exit

_main:
    push ebp
    mov ebp, esp          ; Set up stack frame
    sub esp, 8            ; 16-byte align stack + room for parameters passed
                          ; to functions we call

    mov [esp+4], dword 42 ; Second parameter at esp+4
    mov [esp],dword hello ; First parameter at esp+0
    call _printf

    xor eax, eax          ; Return value = 0
    mov esp, ebp
    pop ebp               ; Remove stack frame
    ret                   ; We linked with C library that calls _main
                          ; after initialization. We can do a RET to
                          ; return back to the C runtime code that will
                          ; exit the program and return the value in EAX
                          ; We can do this instead of calling _exit

Поскольку вы связываетесь с библиотекой C в OS/X, она предоставит точку входа и выполнит инициализацию перед вызовом _main. Вы можете вызвать _exit, но вы также можете выполнить инструкцию RET с возвращаемым программой значением в EAX.


Еще одна потенциальная ошибка NASM?

Я обнаружил, что NASM v2.12 установлен через MacPorts на El Capitan, по-видимому, создает неправильные записи о перемещении для _printf и _exit, а при связывании с окончательным исполняемым файлом код не работает должным образом. Я наблюдал почти те же ошибки, что и вы, с исходным кодом.

Первая часть моего ответа по-прежнему относится к выравниванию стека, однако, похоже, вам также придется обойти проблему NASM. Один из способов сделать это — установить NASM, который поставляется с последними инструментами командной строки XCode. Эта версия намного старше и поддерживает только Macho-32 и не поддерживает директиву default. Используя мой предыдущий код, выровненный по стеку, это должно работать:

section .data
hello   db  "Hello, world %d", 0x0a, 0x00

section .text
;default rel              ; This directive isn't supported in older versions of NASM

global _main
extern _printf, _exit

_main:
    sub esp, 12           ; 16-byte align stack
    mov [esp+4], dword 42 ; Second parameter at esp+4
    mov [esp],dword hello ; First parameter at esp+0
    call _printf

    mov [esp], dword 0    ; First parameter at esp+0
    call _exit

Чтобы собрать с помощью NASM и связать с LD, вы можете использовать:

/usr/bin/nasm -f macho hello32x.asm -o hello32x.o
ld -macosx_version_min 10.8 -no_pie -arch i386 -o hello32x hello32x.o -lc 

В качестве альтернативы вы можете связать с GCC:

/usr/bin/nasm -f macho hello32x.asm -o hello32x.o
gcc -m32 -Wl,-no_pie -o hello32x hello32x.o

/usr/bin/nasm — это местонахождение версии инструментов командной строки XCode для NASM, которую распространяет Apple. Версия, которая у меня есть на El Capitan с последними инструментами командной строки XCode:

NASM версии 0.98.40 (Apple Computer, Inc., сборка 11), собранной 14 января 2016 г.

Я не рекомендую NASM версии 2.11.08, так как в ней есть серьезная ошибка, связанная с macho64. формат. Я рекомендую 2.11.09rc2. Я протестировал эту версию здесь, и, похоже, она работает правильно с приведенным выше кодом.

person Michael Petch    schedule 13.03.2016
comment
Да, это, вероятно, тоже проблема, но, судя по журналу gdb, проблема не в выравнивании. Обычно это кусает людей, только если они используют числа с плавающей запятой, поскольку тогда printf может использовать выровненные инструкции SSE. - person Jester; 13.03.2016
comment
Функции @jester OS/X (особенно в LIBC) довольно привередливы в отношении выравнивания стека, и у вас возникнут проблемы, даже если вы вообще не используете SSE. - person Michael Petch; 13.03.2016
comment
Разве не проблемы, когда исполнение переходит в странные места? В любом случае, я поверю вам на слово, если вы поклянетесь, что проверили это с помощью ld -lc, как это сделал OP, и это сработало. - person Jester; 13.03.2016
comment
@Jester: я видел незнакомца! Я действительно видел это сам много раз. Когда я ответил на этот вопрос, я смог воспроизвести проблему OP, как описано, и когда стек был правильно выровнен, все заработало, как и ожидалось. Я видел ваш комментарий об инициализации LIBC. То, как OP делал то, что он на самом деле правильно связывает с LIBC, и среда выполнения C выполнит свою инициализацию, а затем вызовет _main . Он мог бы упростить связь, используя GCC, как вы указали. - person Michael Petch; 13.03.2016
comment
Хорошо. В Linux точка входа процесса будет установлена ​​на _main, поэтому она не будет вызываться через библиотеку C. ld даже выводит предупреждение cannot find entry symbol _start. Помимо прочего, это означает, что вы не можете просто ret от него, и он не получает аргументы нормально. Я верю вам, что в OSX все по-другому, но звучит странно. Можете ли вы проверить в gdb, чтобы быть уверенным? - person Jester; 13.03.2016
comment
@Jester В Linux точкой входа по умолчанию является _start, и GLIBC предоставляет ее. Затем он вызывает main. OS/X похожа в этом отношении. ret сработало бы вместо вызова exit (я хотел предложить это изменение в его коде, но решил оставить его код как есть), но в качестве эксперимента я удалил вызов exit и добавил 12 в ESP внизу установил EAX со значением (99) и сделал ret. Программа вернулась в оболочку, как и ожидалось, с возвращаемым значением 99. Проходя через отладчик, вы можете увидеть, как она возвращается обратно через код библиотеки C, который изначально вызывал _main. - person Michael Petch; 13.03.2016
comment
Нет, _start нет в -lc в Linux, он находится в объектном файле, который нужно связать отдельно. Похоже, OSX отличается в этом отношении, спасибо за проверку :) - person Jester; 13.03.2016
comment
@Jester К сожалению, я оговорился, я быстро набрал. в Linux вам необходимо предоставить _start через файлы crt или автоматически связать их с помощью GCC . OS/X отличается тем, что вы можете напрямую связываться с LD, используя LIBC для целевой платформы OS/X, которая выполняет точку входа для версии OS/X, для которой вы создаете. Вы можете обойти это поведение при связывании с LD в OSX, предоставив собственную точку входа, но тогда среда выполнения C не будет выполняться автоматически, и тогда вы столкнетесь с потенциальные проблемы, как и в Linux. - person Michael Petch; 13.03.2016
comment
OK @MichaelPetch Я только что скопировал код Майкла дословно в новый файл, собрал его и связал. У меня все еще есть ошибка seg. Я начинаю думать, что что-то не так с моим программным обеспечением для разработки, потому что я не вижу причин, по которым код не будет работать. Я попытался связать с gcc вместо использования ld и получил ту же ошибку. Я уезжаю в командировку, и я не смогу работать над этим в течение недели. Однако я вернусь и сделаю все, что в моих силах, чтобы найти решение. Спасибо за вашу помощь. - person querist; 13.03.2016
comment
@querist: какую версию OSX вы используете? - person Michael Petch; 13.03.2016
comment
@querist: И я также надеюсь, что вы перекомпилировали файл с помощью NASM и сделали ссылку. Я только что попробовал свой код с gcc -m32 hello32x.o -o hello32x -Wl,-no_pie, и он также работал, как и ожидалось. Таким образом, использование как LD, так и GCC здесь сработало. Если я возьму ваш исходный код, у него определенно есть проблемы с вызовом printf, как вы обнаружили. - person Michael Petch; 13.03.2016
comment
Я ошибался в том, что фактическая ошибка в lldb была такой же. Когда я запускаю ваш код, он фактически умирает в printf, но с * thread #1: tid = 0x4452c3, 0x9bbe9440 libdyld.dylib misaligned_stack_error_, queue = 'com.apple.main-thread', stop reason = EXC_BAD_ACCESS (code=EXC_I386_GPFLT) . Единственное, я до сих пор использую OS/X Yosemite 10.10.x. Sp, кажется, есть некоторая разница с вашей средой - person Michael Petch; 13.03.2016
comment
Привет, @MichaelPetch, я управляю El Capitan. Я попробовал код в новом файле, скомпилированном и связанном, как описано выше. Я также попробовал это на машине моего сына с Mountain Lion с гораздо более старой версией nasm, и это сработало. К сожалению, я в аэропорту, еду в командировку, которая продлится неделю, и у меня с собой служебный компьютер, а не мой Mac. Я проверяю наличие обновлений для XCode и связанных инструментов. Я также собираюсь проверить последнюю версию nasm на компьютере моего сына, когда вернусь. Вы молодцы. Еще раз спасибо вам, Jester, и Питеру Кордесу. - person querist; 13.03.2016
comment
@querist Я думаю, возможно, что-то изменилось с El Capitan в этом отношении, но я должен признать, что мог не знать об этом. Если у меня будет возможность установить его сегодня, я попробую. Удачной поездки. - person Michael Petch; 13.03.2016
comment
Спасибо, @MichaelPetch! Сейчас я благополучно в своем отеле, но в тысяче миль от моего Macbook Pro, поэтому я не могу больше ничего сделать с этим, пока не вернусь. - person querist; 13.03.2016
comment
@querist: Кажется, в NASM, который вы, вероятно, используете, есть ошибка. Мне удалось воспроизвести проблему с версией NASM для Macport (2.12) на El Capitan. Кажется, что 2.12 генерирует неправильные записи о перемещении для _printf_ и _exit. Я смог использовать устаревшую версию NASM в /usr/bin/nasm (версия NASM 0.98.40 (Apple Computer, Inc. build 11)), которая поставляется с последней версией XCode и работает нормально. Старая версия не поддерживает 64-битный мачо, но поддерживает 32-битный. Придется сделать пару модов для кода. Добавьте bits 32 в начало файла и удалите директиву default rel - person Michael Petch; 14.03.2016
comment
Привет @MichaelPetch, я переустановлю 2.11, когда вернусь домой. У меня до сих пор где-то есть установщик. - person querist; 15.03.2016
comment
@querist, когда вы вернетесь домой, я рекомендую 2.11.09rc2 (я обновил свой ответ внизу, добавив дополнительную информацию) - person Michael Petch; 15.03.2016
comment
Я понизил, протестировал, и он работает отлично. Спасибо, @MichaelPetch! - person querist; 23.03.2016