LINUX.ORG.RU

inline asm and registers clobbering


0

0

вот такой код:

int R_EAX, R_ESI;

void test(void)
{
	asm volatile ("" : "=a" (R_EAX), "=S" (R_ESI));
}

порождает вот такой ассемблер.

.globl test
	.type	 test,@function
test:
	pushl %esi
#APP
#NO_APP
	movl %eax,R_EAX
	movl %esi,R_ESI
	popl %esi
	ret

вопрос: почему gcc сохраняет/восстанавливает %esi
(в отличие от %eax)? %esi ведь фигурирует только в
output list.

на такое поведение закладывается, например, switch_to(),
но я не могу найти, где это поведение документированно.

★★★★★

Ответ на: комментарий от no1sm

> а если с -О2??

это и было с -O2 -fomit-frame-pointer, забыл написать.

еще раз повторю, переключение стека в schedule(), do_IRQ()
на такое поведение закладываются, поэтому это, видимо,
следует считать нормой.

idle ★★★★★
() автор топика

%esi - это вроде как выходной alias для R_ESI, но не сам выход.
Поэтому он по сути и есть clobbered, разве нет?

Murr ★★
()
Ответ на: комментарий от Murr

Даже не clobbered, а скорее просто временный (фиксированный) регистр.

Murr ★★
()
Ответ на: комментарий от Murr

> %esi - это вроде как выходной alias для R_ESI, но не сам выход.
> Поэтому он по сути и есть clobbered, разве нет?

если бы я хоть чего-то понял в этом :)

смотрите, еах и esi используются одинаково, но esi
сохраняется, а еax - нет. смысла в сохранении esi,
вроде бы нет: мы его только читаем.

можно было бы предположить, что это проблемы с
оптимизацией, но:
#define switch_to(...)
        unsigned long esi,edi;
        ....
        : "=S" (esi),"=D" (edi)
явно это поведение эксплуатирует.

кстати, вот еще код:
void test(void)
{
        unsigned long R_EAX, R_ESI;
        asm volatile ("" : "=a" (R_EAX), "=S" (R_ESI));
}

теперь переменные локальны, и asm вот такой:
test:
        pushl %esi
#APP
#NO_APP
        popl %esi
        ret

> Я в gnu asm не спец

вот и я не того :)

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

>смотрите, еах и esi используются одинаково, но esi
>сохраняется, а еax - нет. смысла в сохранении esi,
>вроде бы нет: мы его только читаем.

esi не только читаем, он еще как выходной регистр помечен.
Как бы ты (ничего что на ты?) пометил, если бы изменял?
Как clobbered нельзя, поскольку он уже используется в других списках.

eax по идее может не сохраняться, потому что gcc в eax ничего не держит (оптимизация).

>теперь переменные локальны, и asm вот такой:
Опять оптимизация, IMHO.
Хотя то, что GCC положил на volatile - неприятно (но ведь в любом случае он тут прав).

Murr ★★
()
Ответ на: комментарий от Murr

> esi не только читаем, он еще как выходной регистр помечен.

да как же не только читаем? или я про этот inline asm
еще меньше знаю, чем думаю?

я полагал, что asm("" : "=b" (variable)), например,
означает просто записать значение ebx в variable.
то есть, сам регистр просто прочитать.

> Как clobbered нельзя, поскольку он уже используется
> в других списках.

хм... проверил, на gcc-3.2.2 действительно нельзя.
мой egcs-2.91.66 позволяет. буду иметь в виду, спасибо.

> > теперь переменные локальны, и asm вот такой:
> Опять оптимизация, IMHO.

правильно, eax оптимизирован, а почему не выброшен
код с esi?

похоже, любое использование "callee saved" регистра
приводит к такому поведению, но:

1. где это документировано?

2. почему это было сделано? компилятор ведь видит, что
esi не меняется, зачем его сохранять/восстанавливать?

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

>да как же не только читаем?

Дык он же в output list, даже значок "=" стоит :-D

>правильно, eax оптимизирован, а почему не выброшен
код с esi?

Ну, наверное, между вызовами функций сохраняются все регистры, кроме eax (в котором обычно return value).

Вообще, странно, что кто-то на это расчитывает, скорее всего это пример плохого кода. Но есть места, где нужно знать о том, как передаются параметры в функцию - без этого никак (в kernel_thread по-моему такая ситуация). Даже если это не документировано, то без этого не обойтись. :)

Murr ★★
()
Ответ на: комментарий от Murr

> > да как же не только читаем?
> Дык он же в output list, даже значок "=" стоит :-D

так output list и означает, что эти регистры мы _читаем_,
тогда как input list, наоборот, говорит, в какие регистры
мы пишем и чего пишем.

или я уже, того... RTFM ? только где она...

> Ну, наверное, между вызовами функций сохраняются все регистры,
> кроме eax

gcc сохраняет ebx,esi,edi только, если портит их.

> Вообще, странно, что кто-то на это расчитывает,

вот именно это меня и удивляет, если бы не это, я бы
предположил, что gcc просто недостаточная оптимизация.

> скорее всего это пример плохого кода

switch_to(), do_IRQ() (переключение стека, bk версия).
довольно важный код, чтобы быть слишком плохим...

ладно, сдаюсь. в код gcc не полезу ни за что на свете,
а документацию хрен найдешь какую.

idle ★★★★★
() автор топика
Ответ на: комментарий от Murr

вот, нашел в первой ссылке.

> Note that GCC will not use a clobbered register for inputs or outputs.
> GCC 2.7 would let you do it anyway, specifying an input in class
> "a" and saying that "ax" is clobbered.  GCC 2.8 and egcs are getting
> picky, and complaining that there are no free registers in class
> "a" available.  This is not the way to do it.  If you corrupt an input
> register, include a dummy output in the same register, the value of which
> is never used.  E.g.
>
>   int dummy;
>   asm("munge %0" : "=r" (dummy) : "0" (input));

но, во-первых, я не делаю "If you corrupt an input register".

во вторых, в gcc документации я - опять-таки - не нашел
подтверждения тому, что регистры в output list считаются
clobbered, и почему это так.

разве что:
> There is no way for you to specify that an input operand is modified without
> also specifying it as an output operand.

но у меня input пустой, и регистр не "modified"

нет, все, сдаюсь.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

> во вторых, в gcc документации я - опять-таки - не нашел
> подтверждения тому, что регистры в output list считаются
> clobbered, и почему это так.

то есть, так оно, похоже и есть. и, наверное, причина в том,
что нет явной возможности указать их в clobber list, как ты
и писал.

но тогда, получается, нельзя на это закладываться - оптимизатор
может поумнеть и заметить, что esi не меняется, и не сохранять
его.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

Кстати, а что там насчет ESI и EDI в switch_to?
у меня компилятор вроде забивает на все, что с ними
связано.

>00000257 <schedule+0x257> mov eax,DWORD PTR [ebx+112]
>0000025a <schedule+0x25a> test eax,eax
>0000025c <schedule+0x25c> je 000003f9 <schedule+0x3f9>
>00000262 <schedule+0x262> mov eax,ebx
>00000264 <schedule+0x264> mov edx,esi
>00000266 <schedule+0x266> pushf
>00000267 <schedule+0x267> push ebp
>00000268 <schedule+0x268> mov DWORD PTR [ebx+600],esp
>0000026e <schedule+0x26e> mov esp,DWORD PTR [esi+600]
>00000274 <schedule+0x274> mov DWORD PTR [ebx+596],0x289
>0000027e <schedule+0x27e> push DWORD PTR [esi+596]
>00000284 <schedule+0x284> jmp 00000285 <schedule+0x285>
>00000289 <schedule+0x289> pop ebp
>0000028a <schedule+0x28a> popf
>0000028b <schedule+0x28b> call 000009bc <io_schedule_timeout+0x5c>

Murr ★★
()
Ответ на: комментарий от Murr

Позволю себе вмешаться... Я вот что думаю (по поводу самого первого
кода). Ведь есть слово volatile - вот компилятор ничего и не
оптимизирует в ассемблерном коде. А popl %esi - ИМХО просто потому, что
%esi - это callee-save регистр, а %eax - caller-save.

Это все ИМХО, прошу больно не пинать :)

jek_
()
Ответ на: комментарий от jek_

jek_:

> Это все ИМХО, прошу больно не пинать :)

обязательно попинал бы, если бы сам разбирался :)

> Ведь есть слово volatile - вот компилятор ничего и не оптимизирует
> в ассемблерном коде.

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

> А popl %esi - ИМХО просто потому, что %esi - это callee-save
> регистр, а %eax - caller-save.

да, я и писал, что такая фигня для callee-save. вопрос-то
_почему_ это он делает, esi ведь не изменяется!

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

А!.. Ну все, дошло. Я и забыл что в AT&T asm принято писать src,dst :)
Ну единственное предположение (ну про это говорили уже, насколько я
понимаю) - volatile запрещает компилятору вообще не то что менять, а
даже смотреть в этот asm-блок. Вот он и не знает, что там меняется,
а что нет.

jek_
()
Ответ на: комментарий от Murr

Murr:
> у меня компилятор вроде забивает на все, что с ними
> связано.

на esi он не плюет, он сохраняет его в edx, похоже.
edi не видно, но, может он раньше его куда-то сунул?

хрен поймешь.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

> edi не видно, но, может он раньше его куда-то сунул?

кроме того, он может "помнить", что push %edi было
уже сделано в прологе, а в эпилоге все равно делать
pop.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

Давай по порядку:

inline код:

>#define switch_to(prev,next,last) do {                                  
>        unsigned long esi,edi;                                          
>        asm volatile("pushfl\n\t"                                       
>                     "pushl %%ebp\n\t"                                  
>                     "movl %%esp,%0\n\t"        /* save ESP */          
>                     "movl %5,%%esp\n\t"        /* restore ESP */       
>                     "movl $1f,%1\n\t"          /* save EIP */          
>                     "pushl %6\n\t"             /* restore EIP */       
>                     "jmp __switch_to\n"                                
>                     "1:\t"                                             
>                     "popl %%ebp\n\t"                                   
>                     "popfl"                                            
>                     :"=m" (prev->thread.esp),"=m" (prev->thread.eip),  
>                      "=a" (last),"=S" (esi),"=D" (edi)                 
>                     :"m" (next->thread.esp),"m" (next->thread.eip),    
>                      "2" (prev), "d" (next));                          
>} while (0)

Дизассемблированный код (Intel):

>0000016d <schedule+0x16d> mov    eax,ebx
>0000016f <schedule+0x16f> mov    edx,esi
>00000171 <schedule+0x171> pushf
>00000172 <schedule+0x172> push   ebp
>00000173 <schedule+0x173> mov    DWORD PTR [ebx+692],esp
>00000179 <schedule+0x179> mov    esp,DWORD PTR [esi+692]
>0000017f <schedule+0x17f> mov    DWORD PTR [ebx+688],0x194
>00000189 <schedule+0x189> push   DWORD PTR [esi+688]
>0000018f <schedule+0x18f> jmp    00000190 <schedule+0x190>
>00000194 <schedule+0x194> pop    ebp
>00000195 <schedule+0x195> popf
>00000196 <schedule+0x196> mov    DWORD PTR [esp],eax

Какой делается вывод:

На входе в switch_to: ebx=prev, esi=next
На выходе из switch_to: last=eax (который вернул __switch_to)
Побочный эффект: в prev->thread.esp сохраняется текущий esp, 
  в prev->thread.eip сохраняется указатель после jmp;
  в esp загружается next->thread.esp, 
  на стеке сохраняется next->thread.eip, чтобы при возврате из 
  switch_to мы попали на следующую инструкцию переключаемого
  процесса (как я понимаю, на этот самый "pop ebp").

Регистры esi и edi не сохраняются, поскольку тут даже нет смысла
их сохранять (это можно детальнее посмотреть через objdump на sched.o), т.е. компилятор тут соптимизировал бредовый код, за
что ему честь и хвала.

Murr ★★
()
Ответ на: комментарий от Murr

Murr:

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

> т.е. компилятор тут соптимизировал бредовый код, за > что ему честь и хвала.

во-первых, почему тогда компилятор не смог оптимизировать приведенный мной код?

во-вторых, не факт, что он соптимизировал. я уже говорил, что он может "помнить", что esi уже сохранен в прологе, а в эпилоге нам его восстанавливать.

пример: void test1(void) { unsigned long R_ESI; asm volatile ("" : "=S" (R_ESI)); } void test2(void) { unsigned long R_ESI1, R_ESI2; asm volatile ("" : "=S" (R_ESI1)); asm volatile ("" : "=S" (R_ESI2)); }

ассемблер: test1: pushl %esi popl %esi ret test2: pushl %esi popl %esi ret

то есть, в test2 он не сохраняет esi второй раз, и это, конечно, оптимизация. но один раз таки это делается!

> Регистры esi и edi не сохраняются, поскольку тут даже нет смысла

а в test1 есть смысл???

что касается упоминания esi/edi в switch_to(), я знаю, в чем дело. в случае, если schedule() держит какую-то переменную в этих регистрах, и если она будет использована после switch_to() (чего на самом деле не происходит) мы _должны_ сохранить эти регистры в стэке prev, и достать их из стэка next, поскольку дальше schedule() выполняться будет в контексте процесса next.

поэтому в 2.4 версии switch_to() у нас яные push/pop стоят. нынешняя версия оптимизированна Линусом, и я не понимаю вот этой gcc магии, как, и почему, и на основании какого документированного поведения это должно работать.

idle ★★★★★
() автор топика
Ответ на: комментарий от Murr

пардон, теперь с форматированием.

Murr:

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

> т.е. компилятор тут соптимизировал бредовый код, за
> что ему честь и хвала.

во-первых, почему тогда компилятор не смог оптимизировать
приведенный мной код?

во-вторых, не факт, что он соптимизировал. я уже говорил,
что он может "помнить", что esi уже сохранен в прологе,
а в эпилоге нам его восстанавливать.

пример:
void test1(void)
{
        unsigned long R_ESI;
        asm volatile ("" : "=S" (R_ESI));
}
void test2(void)
{
        unsigned long R_ESI1, R_ESI2;
        asm volatile ("" : "=S" (R_ESI1));
        asm volatile ("" : "=S" (R_ESI2));
}

ассемблер:
test1:
        pushl %esi
        popl %esi
        ret
test2:
        pushl %esi
        popl %esi
        ret

то есть, в test2 он не сохраняет esi второй раз, и это,
конечно, оптимизация. но один раз таки это делается!

> Регистры esi и edi не сохраняются, поскольку тут даже нет смысла

а в test1 есть смысл???

что касается упоминания esi/edi в switch_to(), я знаю, в чем дело.
в случае, если schedule() держит какую-то переменную в этих регистрах,
и если она будет использована после switch_to() (чего на самом
деле не происходит) мы _должны_ сохранить эти регистры в стэке
prev, и достать их из стэка next, поскольку дальше schedule()
выполняться будет в контексте процесса next.

поэтому в 2.4 версии switch_to() у нас яные push/pop стоят. нынешняя
версия оптимизированна Линусом, и я не понимаю вот этой gcc магии, как,
и почему, и на основании какого документированного поведения это должно
работать.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

idle:

>во-первых, почему тогда компилятор не смог оптимизировать
>приведенный мной код?

it's a kind of magic :)

>во-вторых, не факт, что он соптимизировал. я уже говорил,
>что он может "помнить", что esi уже сохранен в прологе,
>а в эпилоге нам его восстанавливать.

Ну почему ты так не хочешь посмотреть в sched.o? :)

Не вставлять же мне его в форум... Там дальше по коду используются esi и edi без всякого восстановления из стека. То есть, как я понимаю, соответствующий inline говорит "мы портим esi и edi, используя их как временные регистры" (кстати, ты не интересовался, почему именно esi и edi, а не другие регистры?), а уже дело компилятора решать сохранять ему их или нет, поэтому расчитывать на то, что что-то лежит в стеке или не лежит - нельзя, просто можно быть уверенным, что если компилятору понадобятся esi и edi, то он их сможет восстановить (или просто не будет в них ничего кэшировать - что, кстати, более разумно).

Murr ★★
()
Ответ на: комментарий от idle

В твоих примерах, IMHO, компилятор не может понять нужен ли ему будет ESI или нет вне функций (он же не анализирует поток управления настолько), поэтому он и сохраняет "портящийся ESI" на стеке. В случае с schedule функция switch_to "заинлайнена" в середине schedule, поэтому сохранение ESI и EDI не является побочным эффектом switch_to - ESI и EDI портит сам код schedule (он же его и восстанавливает).

Murr ★★
()
Ответ на: комментарий от Murr

Грубо говоря для моего ядра и компилятора, как я понимаю, пометка esi и edi "портящимися" в контексте schedule не дает компилятору никаких указаний, кроме того, что в них ничего нельзя кэшировать (специальное сохранение их на стеке не имеет большого смысла).

Murr ★★
()
Ответ на: комментарий от Murr

Если ты под прологом и эпилогом имел в виду сохранение ESI и EDI в контексте schedule, то скорее всего так оно и есть, но это, IMHO, не имеет отношение к switch_to.

Murr ★★
()
Ответ на: комментарий от Murr

> Ну почему ты так не хочешь посмотреть в sched.o? :)

как это? смотрел много раз, только компилировал cc -S,
так привычнее.

> Там дальше по коду используются esi и edi без всякого
> восстановления из стека.

я такого не видел! правда, под рукой сейчас только 2.95.3

> почему именно esi и edi, а не другие регистры?)

это, кстати, еще один вопрос. собственно, для меня эта
проблема началась с того, что ebx _не_ сохраняется таким
образом, и у меня была досадная наколка с schedule().

> говорит "мы портим esi и edi, используя их как временные регистры"
> а уже дело компилятора решать сохранять ему их или нет
> поэтому расчитывать на то, что что-то лежит в стеке или не лежит
> - нельзя

вот, у меня складывается впечатление, что так оно и есть.
и теперь мы возвращаемся к истокам: где документировано,
что указав регистр только в output list, мы получим
такое поведение? и почему, вместо этого, просто не добавить
esi/edi в clobbered list?

> it's a kind of magic :)

очень блэск мэджик, при том.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

Ну вроде как из дизассемблированного кода следует, что это не более, чем clobbering. Почему не был написан прямолинейный clobber list?

Ну можно пофантазировать:
1) Линус хотел оптимизировать лишние push, но про clobber lists к тому времени еще не прочитал
2) Линус хотел оптимизировать лишние push, но в текущем gcc была какая-то ошибка с clobber lists (подобный подход хорошо виден в fs/ntfs/compress.c с барьером)
3) Линус хотел оптимизировать лишние push, при этом подчеркнуть, что, если нужно сохранять esi и edi, то нужно это делать в памяти (т.к. gcc очень агрессивно оптимизирует даже volatile код, а в нем меняется esp). При этом, как мы видим, gcc плюет на это указание и посему оно не имеет смысла

В принципе, если ты говоришь, что был соответственный commit, то почему б не посмотреть логи репозитария? Или они закрыты даже на просмотр для простых смертных?

Murr ★★
()
Ответ на: комментарий от Murr

> При этом, как мы видим, gcc плюет на это указание и посему
> оно не имеет смысла

нет, смысл оно точно имеет. то есть, сейчас из switch_to()
это можно убрать, т.к. schedule() и вызываемые inline функции
вообще не используют никаких локальных переменных после вызова
switch_to(), но это будет "by pure lack".

кроме 'prev', конечно, но она явно вычисляется в switch_to().

> В принципе, если ты говоришь, что был соответственный commit,
> то почему б не посмотреть логи репозитария? 

да смотрел я...

http://linux.bkbits.net:8080/linux-2.5/cset@1.889.225.1

комментарий к system.h:

> Clarify "switch_to()" macro, and avoid unnecessary register saves
> and restores. Make it properly set the "last" parameter to the
> previous task.

в этом патче много всяких мелких фиксов.

да и вопрос-то не про switch_to() а про gcc.

idle ★★★★★
() автор топика
Ответ на: комментарий от idle

>нет, смысл оно точно имеет. то есть, сейчас из switch_to() это можно убрать

не убрать, а поместить в clobber list. Почему после этого все должно перестать работать? :) Я могу даже вечером попробовать, можно и локальные переменные ввести. ;)

Murr ★★
()
Ответ на: комментарий от Murr

> не убрать, а поместить в clobber list.

так и я о том же! почему не clobber?

> Почему после этого все должно перестать работать?

не перестанет, даже если просто убрать. (я думаю).
потому, что schedule() и вызываемые inline функции
после вызова switch_to() не используют локальных
переменных, кроме 'next', которая явно вычисляется.

idle ★★★★★
() автор топика
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.