Указатель / арифметика памяти в сборке

#c #assembly

#c #сборка

Вопрос:

Я пытаюсь разобраться в сборке, но есть одна, вероятно, очень простая вещь, которую я не понимаю.

Рассмотрим следующий простой пример

 long long * values = new long long[2];
values[0] = 10;
values[1] = 20;

int j = -1;

values[j 2] = 15;  // xxxxxxx
  

Теперь последняя строка (отмеченная xxxxxx) разбирается на:

 000A6604  mov         eax,dword ptr [j]  
000A6607  mov         ecx,dword ptr [values]  
000A660A  mov         dword ptr [ecx eax*8 10h],0Fh  
  

Первый вопрос: что на самом деле хранится в eax и ecx, это фактические значения (т.Е. -1 для «j» и два длинных длинных значения 10 и 20 для «значений»), или это просто адрес памяти (например, что-то вроде amp;p , amp;values ), указывающий на некоторыеместо, где хранятся значения?

Второй вопрос, я знаю, что должна делать третья строка, но я не совсем уверен, почему это действительно работает. Насколько я понимаю, он копирует значение 0x0F в указанную ячейку памяти. Ячейка памяти — это, по сути, местоположение первого элемента, хранящегося в ecx, плюс размер long long в байтах (= 8) * значение eax (которое равно j, поэтому -1) — плюс общее смещение в 16 байт (в 2 раза больше размера long long). Чего я не понимаю: в этом выражении ecx представляется адресом памяти, в то время как eax представляется значением (-1). Как это возможно? Поскольку они были определены практически одинаково, не должны ли eax и ecx либо содержать адреса памяти, либо оба значения?

Спасибо.

Комментарии:

1. Адрес памяти и значение — это просто биты. Разница только в том, что представляют эти биты.

2. Что касается первого вопроса: он загружает значения j и values соответственно. Значение values , в свою очередь , является адресом фрагмента памяти. Написать что-то подобное mov ecx, OFFSET values было бы похоже на получение адреса указателя в C, который дает вам указатель на указатель.

3. Обратите внимание на это j , и values в вашем коде C тоже есть разные типы.

4. Ах, конечно, это имеет смысл. j = -1, но значения = адрес памяти. Я путал значения со значениями *. Хорошо, я думаю, это объясняет оба вопроса, большое вам спасибо!

Ответ №1:

eax и ecx являются регистрами — первые две инструкции загружают в эти регистры значения, используемые при вычислении, т.Е. j и values (где values означает базовый адрес массива с этим именем).

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

Инструкция mov dword ptr [ecx eax*8 10h],0Fh означает перемещение значения 0Fh (т.Е. 15 десятичных знаков) в местоположение ecx eax*8 10h . Чтобы понять это, рассмотрим каждую часть:

  • ecx является базовым адресом values массива

  • eax является ли значение равным j , то есть -1

  • eax*8 j преобразуется в смещение в байтах — размер a long long равен 8 байтам

  • eax*8 10h 10h — это 16 десятичных знаков, то есть 2 * 8, поэтому оно j 2 преобразуется в смещение в байтах

  • ecx eax*8 10h добавляет это окончательное смещение к базовому адресу массива, чтобы определить местоположение, в котором будет храниться значение 15

Комментарии:

1. Очень хороший ответ, спасибо! И последний вопрос: как он «знает», что j (или eax, соответственно) подписан? Определение j было «mov dword ptr [j],0FFFFFFFFh». Таким образом, 0xFFFFFFFF равняется 4 294 967 295, если интерпретируется как число без знака (и действительно, это то, что сообщает мне окно просмотра в VC). Но я предполагаю, что 4,294,967,295 * 8 вызовет переполнение или даст что угодно, но не -8, как предполагалось. Итак, как это работает ?!

2. 0xFFFFFFFF * 8 is 0x7FFFFFFF8 , который действительно слишком велик, чтобы поместиться в 32-разрядное число (т.Е. Указатель), поэтому используются только младшие значащие 32 бита. Это оставляет нас 0xFFFFFFF8 , т.е. -8 десятичных знаков. Добавьте 2*8 к этому, и вы получите 0x00000008 , какую сумму нужно добавить values , чтобы получить адрес values[-1 2] .

3. Отлично, понял. Однако откуда он знает, что 0xFFFFFFF8 означает -8, а не 4 294 9672,88? Например. подписанный int a = -1 = 0xFFFFFFFF и unsigned int b = 4294967295 = 0xFFFFFFFF . Но доступ к значениям [a 1] и значениям [b 1] выдает один и тот же код сборки; так как же он узнает, обращаться ли к элементу 0 или элементу 4 294 967 296?

4. Процессор не «знает» — это всего лишь биты. Компилятор (точнее, люди, которые написали компилятор) знает, как использовать дополнительную арифметику 2 для реализации отрицательных чисел. Возможно, вам придется поиграть с некоторыми примерами, чтобы действительно понять. Во-первых, убедитесь, что вы знаете, как использовать дополнение 2 (т. Е. инвертировать все биты и добавить 1). Пример: 0x12345678 - 0x8 == 0x12345678 0xFFFFFFF8 == 0x112345670 , это только 0x12345670 в том случае, если вы ограничены младшими значащими 32 битами.

5. Кажется, что не хватает ‘mov dword ptr [ecx eax* 8 14h],00h’, чтобы обнулить верхнюю половину значений [j 2], поскольку это длинное длинное (64-битное или qword).