Как процессоры узнают порядок значений регистров?

В ассемблере можно передавать значения через менее изменчивые регистры или изменчивые регистры. Например, я могу передать аргументы printf, используя edi и esi. Вместо этого я могу использовать ebx и ecx. Этот пример — очень простой надуманный. Мне больше любопытно, как это работает с гораздо более сложными программами, вызывающими несколько функций из libc.

Например, в атаках с возвратно-ориентированным программированием злоумышленник может использовать гаджеты для использования тех же регистров, которые использовались для предыдущей функции, чтобы вставлять в них новые значения из стека, а затем возвращаться к другой функции libc, которая использует те же регистры, например с write и read можно использовать pop rsi в атаках ROP, чтобы использовать любую функцию, если они просочились в глобальную таблицу смещений. Мой общий вопрос можно задать так:

Если злоумышленник наследует регистры от предыдущего вызова read следующим образом:

    0x00005555555552d0 <+107>:   lea    rcx,[rbp-0xd0] <- Memory address of buffer "msg"
    0x00005555555552d7 <+114>:   mov    eax,DWORD PTR [rbp-0xe4] <- contains client fd 0x4
    0x00005555555552dd <+120>:   mov    edx,0x400 <- 1024 (size of bytes to write to memory location/buffer)
    0x00005555555552e2 <+125>:   mov    rsi,rcx
    0x00005555555552e5 <+128>:   mov    edi,eax
    0x00005555555552e7 <+130>:   call   0x5555555550d0 <read@plt>

Как процессор узнает, какие аргументы передать для записи, если регистры, переданные для записи, различны:

    0x00005555555552b1 <+76>:    call   0x555555555080 <strlen@plt>
    0x00005555555552b6 <+81>:    mov    rdx,rax <- store return value from strlen into rdx
    0x00005555555552b9 <+84>:    lea    rcx,[rbp-0xe0] <- message to write
    0x00005555555552c0 <+91>:    mov    eax,DWORD PTR [rbp-0xe4] <- client file descriptor
    0x00005555555552c6 <+97>:    mov    rsi,rcx
    0x00005555555552c9 <+100>:   mov    edi,eax
    0x00005555555552cb <+102>:   call   0x555555555060 <write@plt>  

Понятно, что read не использует rdx, а write не использует edx, так как же процессор узнает, что выбрать, например, если злоумышленник использовал только гаджет, который вставляет значение в rsi?

Я не могу понять, как процессор знает, из каких регистров выбирать (rdx или edx). Как процессоры выбирают значения для передачи libc функциям или функциям/процедурам, если уж на то пошло?

Это не выбор процессора. Он определяется соглашением о вызовах. Обратите внимание, что поскольку read и write принимают одинаковое количество и тип аргументов, они используют одни и те же регистры. В Linux x86-64 это rdi, rsi и rdx для аргументов fd, buf и count соответственно. Непонятно, почему вы думаете, что они разные.

Jester 21.12.2020 03:11

В частности, для вашего примера writemov eax,DWORD PTR [rbp-0xe4] не может быть подсчетом, так как он переносится на edi. Это явно дескриптор файла. Счетчик уже введен в rdx более ранним кодом, который вы не показали.

Jester 21.12.2020 03:14

Я вижу, что изменил второй пример.

asd40732 21.12.2020 03:26

Поэтому, поскольку я упустил из виду, что eax был перемещен в edi, я не увидел, что rcx также является аргументом. Это все еще не объясняет разницу между rcx и edx. Это разные регистры, так как же процессор узнает, какой из них использовать?

asd40732 21.12.2020 03:29
rcx тоже не аргумент (в данном случае). Он перемещен в rsi. Ни rax, ни rcx не используются для передачи аргументов. В показанном коде они просто временные, они перемещаются в правильные регистры аргументов перед вызовом функции.
Jester 21.12.2020 03:30

Хорошо, я вижу, если вы добавите это как ответ, я приму это.

asd40732 21.12.2020 03:32

Да, порядок имеет значение. Порядок, в котором они сопоставляются с аргументами, а не порядок, в котором вы загружаете значения. См. документацию по соглашению о вызовах

Jester 21.12.2020 03:34

@Jester, так как процессор узнает, что нужно выбирать между rdx и edx?

asd40732 21.12.2020 03:57
Почему в Python есть оператор &quot;pass&quot;?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Коллекции в Laravel более простым способом
Коллекции в Laravel более простым способом
Привет, читатели, сегодня мы узнаем о коллекциях. В Laravel коллекции - это способ манипулировать массивами и играть с массивами данных. Благодаря...
JavaScript Вопросы с множественным выбором и ответы
JavaScript Вопросы с множественным выбором и ответы
Если вы ищете платформу, которая предоставляет вам бесплатный тест JavaScript MCQ (Multiple Choice Questions With Answers) для оценки ваших знаний,...
Массив зависимостей в React
Массив зависимостей в React
Все о массиве Dependency и его связи с useEffect.
Toor - Ангулярный шаблон для бронирования путешествий
Toor - Ангулярный шаблон для бронирования путешествий
Toor - Travel Booking Angular Template один из лучших Travel & Tour booking template in the world. 30+ валидированных HTML5 страниц, которые помогут...
2
8
179
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

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

Ответ принят как подходящий

Процессор ничего не знает; регистры не индексируются, и единственный порядок, который они имеют в отношении ЦП, - это номера регистров, используемые в машинном коде. (И для таких вещей, как инструкции сохранения нескольких регистров, такие как устаревший 32-битный режим pusha / popa или xsave для сохранения состояния FPU / SIMD.)

То, что ищет аргументы в определенных местах вызываемой функции, это... дополнительный код (программное обеспечение), сгенерированный компилятором, который скомпилировал функцию с ее аргументами, объявленными определенным образом. Помните, что printf — это еще одно программное обеспечение, не встроенное в процессор.

Компилятор знает стандартное соглашение о вызовах для целевой платформы (в данном случае оно определено в X86-64 System V ABI), поэтому, если и вызывающий, и вызываемый объект соглашаются с соглашением о вызовах, код вызова помещает аргументы в места, которые вызываемые смотреть на них.

Стандартизация этого соглашения о вызовах — это то, как мы можем связать код из разных компиляторов в одну программу и выполнять вызовы в библиотеки.

Кстати, то же самое касается системных вызовов; вы помещаете номер вызова в определенный регистр и запускаете инструкцию, которая переключается в режим ядра (например, syscall). Теперь ядро ​​работает и может просматривать значения, оставшиеся в регистрах. Он использует номер вызова для индексации таблицы указателей функций, вызывая ее с другими аргументами в стандартных регистрах передачи аргументов. (Или туда, куда им нужно перейти в соответствии с соглашением о вызовах C, которое обычно отличается от соглашения о вызовах системных вызовов.)

Каковы соглашения о вызовах для системных вызовов UNIX и Linux (и функций пользовательского пространства) на i386 и x86-64

Я просто не понимаю, как процессор знает, что выбрать rdx или edx. Если вы можете ответить, что я приму ваш ответ.

asd40732 21.12.2020 03:55

@asd40732: О каком выборе вы говорите? При декодировании машинного кода разница между add eax, edx и add rax, rdx составляет 1 бит в префиксе REX, который выбирает 64-битный размер операнда (REX.W=1). dx/edx/rdx имеют одинаковый номер регистра в машинном коде, размер задается атрибутом размера операнда инструкции (префиксы и код операции). wiki.osdev.org/X86-64_Instruction_Encoding#REX_prefix.

Peter Cordes 21.12.2020 04:09

@ asd40732: Если вы имеете в виду, как компилятор решает, какой размер операнда использовать, обычно он соответствует ширине типа переменной C. (Запись 32-битного регистра неявно расширяет нулем до 64-битного, поэтому mov edx, 1234 компилятор будет передавать size_t или uint64_t аргумент, например, 3-й аргумент write; Зачем выполнять инструкции x86-64 для 32-битных регистров? обнулить верхнюю часть полного 64-битного регистра?). Но решение принимает компилятор, а не процессор, как объяснили я и old_timer в обоих наших ответах.

Peter Cordes 21.12.2020 04:09

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