Главная страница
    Top.Mail.Ru    Яндекс.Метрика
Форум: "Прочее";
Текущий архив: 2007.04.08;
Скачать: [xml.tar.bz2];

Вниз

Трассировка exception в Delphi   Найти похожие ветки 

 
Ketmar ©   (2007-02-28 04:00) [40]

> Kerk ©   (27.02.07 22:25) [32]
так я ж и пытался пояснить, что "нэ трэба!" (ц) %-)

> Alex Konshin ©   (28.02.07 02:34) [36]
> реализовать парсер TD32

где ж ты был неделю назад?! когда у меня ещё был жив раздел, на котором жили мои исходники? в том числе и выдраный из JCL (после чего переписаный почти заново) парзер TD32 debug info?! всё как всегда... а опять "втыкать" в формат (хоть он и простой) да писать нет желания. и времени тоже.


 
ZeroDivide ©   (2007-02-28 08:43) [41]

Проблема в том, что в *.map попадают далеко не все имена функций. Поэтому в распечатку могут попасть функции, которые в памяти перед нужной функции,
что еще более некрасиво.


Какие туда не попадают?


 
Alex Konshin ©   (2007-02-28 08:59) [42]

Я точно не понял какие. Но есть не все. Ну точно попадают имена public методов. Причем дело похоже не только в том что компилятор не включает неиспользуемые методы и функции. Я нарывался на ситуацию, когда адрес не принадлежал ни одной из функции.


 
Piter ©   (2007-02-28 09:16) [43]

Ketmar ©   (27.02.07 21:12) [31]
ты без этого жил? жил. значит -- не нужна


уверяю тебя, ты когда-то без Delphi жил, и ничего. Получается, она тебе не нужна.

Kerk ©   (27.02.07 22:25) [32]
Не спорь ты с ним. Он ведь правда похоже считает, что суть в передаваемых в функции аргументах :)


ценю твое великое желание подколоть, но я спрашивал.


 
novill ©   (2007-02-28 09:46) [44]

> [39] Alex Konshin ©   (28.02.07 03:16)

Оттрассировал... оказалось этот блок парсилки "пережевывает" весь map файл до конца, что в общем не удивительно.

 // Parse detailed map of segments
 repeat
   pEOL := NextEOL;
   ptr := FTextPtr;
   linelen := pEOL-ptr;
   if linelen<60 then
   begin
     if (linelen=PUBLICS_BY_NAME_TITLE_LEN) and CompareMem(ptr,PChar(PUBLICS_BY_NAME_TITLE),PUBLICS_BY_NAME_TITLE_LEN) then Break;
     Continue;
   end;
   if not CompareMem(ptr,SEGMENT_CODE_PREFIX,6) then Continue;

   Inc(ptr,6);
   uOffset := GetHexNumber(ptr);
   Inc(ptr,9);
   uSize := GetHexNumber(ptr);

   pUnitName := FTextPtr+59;
   ptr := NextBlank(pUnitName);
   AddUnit( FStringPool.CopyString(pUnitName,ptr-pUnitName), uOffset, uSize );
 until NextLine=nil;


 
novill ©   (2007-02-28 10:46) [45]

Alex Konshin ты в какой версии Дельфи это всё тестил?

В .map файле созданном в Д6
строки
 Address         Publics by Name
 Address         Publics by Value

имеют отличие от ожидаемого парсером написание.


 
Игорь Шевченко ©   (2007-02-28 10:48) [46]

Alex Konshin ©   (28.02.07 02:42) [37]


> А что хоть происходит?
> Ну у меня это сделано в сервисе, так что я с формами не
> тестировал, всякое может быть. Но по идее должно работать.
>
> Ты включи отладку самого стектрейсера (в defines.inc), возможно
> поймешь в чем дело. Я на вскидку, конечно, не знаю в чем
> дело, но возможно, что трейсер не может найти конец место,
>  где надо остановится раскручивать. Хотя, черт его знает
> что там у тебя случилось - ты же ничего конкретного не сообщил.
>


Происходит, как и требуется, Access Violation, но DbgView ничего не показывает, а writeln в VCL-приложении мне даже вставлять не хочется :)
На FormCreate вызывается HookException (ODS), по ButtonClick вызывается метод, гарантирующий Access Violation

В консольном все хорошо, показывает и в окне консоли и в dbgview.


 
Alex Konshin ©   (2007-02-28 10:58) [47]


> novill ©   (28.02.07 10:46) [45]
>
> Alex Konshin ты в какой версии Дельфи это всё тестил?
>
> В .map файле созданном в Д6
> строки
>  Address         Publics by Name
>  Address         Publics by Value
> имеют отличие от ожидаемого парсером написание.

Вот и хорошо, что ты это нашел :) Нужно добавить условную компиляцию.
Я свой проект под BDS 4.0 компилю. Я же сказал, что я это делал для себя и для конкретного проекта. Но можно довести до ума совместными усилиями :)


 
Игорь Шевченко ©   (2007-02-28 11:00) [48]

Alex Konshin ©   (28.02.07 10:58) [47]


> Я свой проект под BDS 4.0 компилю


Кстати, описанное мной поведение наблюдается тоже в D2006 (другой версии просто нету)


 
Alex Konshin ©   (2007-02-28 11:02) [49]

> Игорь Шевченко ©   (28.02.07 10:48) [46]
> Происходит, как и требуется, Access Violation, но DbgView
> ничего не показывает, а writeln в VCL-приложении мне даже
> вставлять не хочется :)
> На FormCreate вызывается HookException (ODS), по ButtonClick
> вызывается метод, гарантирующий Access Violation
>
> В консольном все хорошо, показывает и в окне консоли и в  dbgview.

У меня такая мысль: а сам Delphi наверно тоже в какой-то момент устанавливает свой обработчик прерываний. Наверно в этом и дело. Он затирает мой хук.
Вообще-то в оригинале в JCL делается организуется цепочка хуков, а так мне это было нафиг не нужно, то я это и не стал делать. Короче понятно, в каком месте и куда копать.


 
Alex Konshin ©   (2007-02-28 11:12) [50]

А, так ты по FormCreate хук ставишь? Ставь его в *.dpr в самом начале.


 
Игорь Шевченко ©   (2007-02-28 11:21) [51]

Alex Konshin ©   (28.02.07 11:12) [50]

Это я сейчас не могу попробовать, вечером :)


 
ZeroDivide ©   (2007-02-28 12:10) [52]

>Я точно не понял какие. Но есть не все.

Думаю, все же, есть имена ВСЕХ методов вызываемых в проекте.

>Я нарывался на ситуацию, когда адрес не принадлежал ни одной из функции.
Возможно Exception возникал внутри одной из стандартных виндовских dll, тогда да, адрес exception"а в map-файле не найдешь. В данном случае важен список параметров вызова, который тоже достаточно трудно вытащить из стека. Но история вызовов может дать некоторую информацию.


> FormCreate хук ставишь? Ставь его в *.dpr в самом начале.

Я код не смотрел... зачем вообще для этой задачи хуки?


 
Rouse_ ©   (2007-02-28 12:28) [53]


>  я, например, вменяемый способ отлаживать некий сервис так
> и не нашёл.

Эээ, Attach to Process не пробовал? :)


 
novill ©   (2007-02-28 12:33) [54]

теперь вываливается AV  на строчке
AddPublicName( sName, FStringPool.CopyString(pName,pEOL-pName), uOffset );
при вызове CopyString

Строчки
     Result := PChar(Allocate(Len+1));
     System.Move(Ptr^,Result^,Len);


при Len=4294962803 не срабатывают,  на (Result+Len)^ := #0; валится AV.


 
ZeroDivide ©   (2007-02-28 12:35) [55]

Вот как делаю я:

Для того чтобы оттрассировать стек, я просто получаю его адрес при старте проекта и при возникновении ошибки, а затем пробегаюсь по нему на предмет разделения его содержимого на данные и адреса возврата и затем строю историю вызовов, проверяя по map-файлу каким функциям принадлежат найденные адреса возврата.
И ни каких хуков не надо: код работатет только при возникновении unhandled exception!

Самое сложное в этой ситуации, отделить данные от адресов возврата. Это возможно:

Кусок из моего TYanosExceptionHandler (с):

procedure TYanosExceptionHandler.GetCalls(var St: TstringList);
var
 MapFileAddress, PrevProcStart, NextProcStart, ProcStart: DWORD;
 StackPtr: DWORD;
 n: integer;
 m: LongInt;
 sModule, sProc, sLine, ComStr: String;
 RepeatCnt: Integer;
begin
 St.Add(" ");
 MapFileAddress := GetMapAddressFromAddress(DWORD(ExceptAddr));
 St.Add("Module= "+ GetModuleNameFromAddress(MapFileAddress));
 St.Add("Proc= "+ GetProcNameFromAddress(MapFileAddress, ProcStart));
 St.Add("Line= "+ GetLineNumberFromAddress(MapFileAddress));
 St.Add(" ");

 asm
   mov StackPtr, ESP
 end;

 St.Add(" ИСТОРИЯ ВЫЗОВОВ ПРОЦЕДУР");
 m := DWORD(FStack);
 m := m - DWORD(StackPtr);
 RepeatCnt := 1;
 PrevProcStart := 0;
 if m > 0 then
   for n := 0 to m do
   begin
     if IsCall(StackPtr, ComStr, NextProcStart) then
     begin
       MapFileAddress := GetMapAddressFromAddress(DWORD(Ptr(StackPtr)^)-5);
       sModule := GetModuleNameFromAddress(MapFileAddress);
       sProc := GetProcNameFromAddress(MapFileAddress, ProcStart);
       sLine := GetLineNumberFromAddress(MapFileAddress);
       if (sModule <> "") and (sProc <> "") and (sLine <> "") and (sModule <> "YanosExceptionHandler") then
       begin
         if ehoCallHistoryExt in Options then
         begin
           if PrevProcStart = ProcStart then
             Inc(RepeatCnt)
           else
           begin
             if RepeatCnt > 1 then
               St.Add("Recursive, count =" + IntToStr(RepeatCnt));
             RepeatCnt := 1;
           end;
           if RepeatCnt = 1 then
           begin
             St.Add("  ");
             St.Add("Module= "+ sModule);
             St.Add("Proc= "+ sProc);
             St.Add("Start at= $"+ IntToHex(ProcStart, 8));
             St.Add("Line= "+ sLine);
             St.Add("Comment= "+ComStr);
             if NextProcStart <> 0 then
               St.Add("Call to $" + IntToHex(NextProcStart, 8));
             PrevProcStart := ProcStart;
           end;
         end
         else
         begin
           if St[St.Count-1] <> sProc then
             St.Add(sProc);
         end;
       end;
     end;
     Inc(StackPtr);
   end;
end;

function TYanosExceptionHandler.IsCall(StackPtr: DWORD; var ComStr: String; var NextProcStartAddr: DWORD): boolean;
var
 SAddr: DWORD;
 CallPtr: ^DWORD;
 CallData: DWORD;
//  Found: boolean;
 function GetRegisterName(b: Byte): string;
 begin
   case b of
     0: Result := "EAX";
     1: Result := "ECX";
     2: Result := "EDX";
     3: Result := "EBX";
     4: Result := "ESP";
     5: Result := "EBP";
     6: Result := "ESI";
     7: Result := "EDI";
   end;
 end;

 function GetRegisterValue(b: Byte): DWORD;
 begin
   case b of
     0: asm mov Result, EAX end;
     1: asm mov Result, ECX end;
     2: asm mov Result, EDX end;
     3: asm mov Result, EBX end;
     4: asm mov Result, ESP end;
     5: asm mov Result, EBP end;
     6: asm mov Result, ESI end;
     7: asm mov Result, EDI end;
   end;
 end;

begin
 Result := False;

   //Near Address Call (Normal)
   SAddr := DWORD(Ptr(StackPtr)^);
   Dec(SAddr, 5);
   CallPtr := Ptr(SAddr);
   if IsBadHugeReadPtr(CallPtr,5) = False then
   begin
     CallData := BYTE(CallPtr^);
     if CallData = $E8 then
     begin
       ComStr := "Call Address";
       Result := True;
       Inc(SAddr);
       CallPtr := Ptr(SAddr);
       NextProcStartAddr := DWORD(CallPtr^) + DWORD(Ptr(StackPtr)^);
     end;
   end;

   // Register addresing call with offset (call [EAX+X] - call [EDI+X])
   SAddr := DWORD(Ptr(StackPtr)^);
   Dec(SAddr, 3);
   CallPtr := Ptr(SAddr);
   if IsBadHugeReadPtr(CallPtr,3) = False then
   begin
     CallData := WORD(CallPtr^);
     if (Lo(WORD(CallData)) = $FF) and (Hi(WORD(CallData)) >= $50) and (Hi(WORD(CallData)) <= $57) then
     begin
       Result := True;
       Inc(SAddr,2);
       CallPtr := Ptr(SAddr);
       if ShortInt(CallPtr^) < 0 then
         ComStr := "CALL ["+ GetRegisterName(Byte(StrToInt("$50") xor Hi(WORD(CallData))))+ IntToStr(ShortInt(CallPtr^)) +"]"
       else
         ComStr := "CALL ["+ GetRegisterName(Byte(StrToInt("$50") xor Hi(WORD(CallData))))+"+"+ IntToStr(ShortInt(CallPtr^)) +"]";
       NextProcStartAddr := 0; //DWORD(Ptr( GetRegisterValue(Byte(StrToInt("$50") xor Hi(WORD(CallData)))) + ShortInt(CallPtr^) )) ;
     end;
   end;

end;


 
Ketmar ©   (2007-02-28 12:49) [56]

> Rouse_ ©   (28.02.07 12:28) [53]
неа, не канает. сервис занимается в том числе перехватом/подменой API. и сильно многопоточный. с attach не будет ничего, кроме геморроя. %-)


 
Alex Konshin ©   (2007-02-28 12:49) [57]

Удалено модератором
Примечание: Выражения выбираем


 
Alex Konshin ©   (2007-02-28 13:15) [58]

> ZeroDivide ©   (28.02.07 12:35) [55]

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

Но вообще-то никто не мешает скомбинировать эти два способа раскрутки для взаимной коррекции/контроля и извлечения большей информации. Теоретически это можно сделать.


 
pasha_golub ©   (2007-02-28 13:20) [59]

Извините, ребята, если повторю кого, но читать все сил нету.

http://eurekalog.com/

Вот эта вещь просто иногда спасает.


 
Alex Konshin ©   (2007-02-28 13:37) [60]


> Alex Konshin ©   (28.02.07 12:49) [57]
>
> Удалено модератором
> Примечание: Выражения выбираем
>

Очень интересно, что же я там такого сказал? Прямо теряюсь в догадках


 
Ketmar ©   (2007-02-28 13:39) [61]

> pasha_golub ©   (28.02.07 13:20) [59]
Prices begin at $99(US).


 
novill ©   (2007-02-28 13:56) [62]

> [60] Alex Konshin ©   (28.02.07 13:37)

Да вроде довольно культурно всё, кроме одного слова. Типа "ерунда", только эмомоинальнее

Цитировать не буду...


 
novill ©   (2007-02-28 13:57) [63]

процтитирую все же

Я тоже думал, что все. Ан нет. Говорю же, уже сталкивался. Я сначала тоже сделал печать имени метода/функции и смещения от ее начала, но увидел, что иногда <censored> получается. Почему - не разбирался, мне оп-большому счету это пофиг было, я и тому, что номера строк стало выдавать, был безумно рад.

> Я код не смотрел... зачем вообще для этой задачи хуки?

Это не те хуки, что ты думаешь. Там действительно подменяется RaiseException в   kernel32 для перехвата железных исключений. А для перехвата дельфийских  достаточно установить Sustem.ExceptObjProc.
Перехват делается я так понимаю для того, чтобы первыми получить информацию о прерывании и о его месте, когда стек еще не испорчен самими обработчиками.
Этот эффект кстати хорошо видно и в Delphi IDE когда происходит прерывание. После прерывания зачастую уже и не поймешь что произошло.
Но с другой стороны тебя никто и не заставляет использовать этот хук. Если тебе достаточно места самого прерывания, то можно достать его адрес, а по адресу уже найти в карте место. Но обычно этого недостаточно.


 
ZeroDivide ©   (2007-02-28 14:37) [64]


Я не понимаю, каким образом в общем случае можно сделать раскрутку стека полагаясь только на call и ret. Да, зачастую это будет работать.


Доставайте и ложите что хотите, если не трогаете адреса возврата. Если трогаете и при exception не возвращаете их назад, то лучше отказаться от такого стиля программирования. Если возвращаете их назад, то это уже обрабатываемое исключение.
В общем случае МОЖНО сделать раскрутку стека полагаясь только на call и ret... именно так это делает процессор :)


 
ZeroDivide ©   (2007-02-28 14:46) [65]

Одно исключение: если вы случайно, в качестве каких-либо данных, запихаете в стек значение совпадающее с адресом возврата. Шанс такой ситуации крайне мал. (Во всяком случае, на ранних стадиях нового проекта, я получаю штук по 10 багрепортов в день, но такой ситуации пока в моей практике не пока возникло.)
IsBadCodePtr еще больше сокращает этот шанс.


 
ZeroDivide ©   (2007-02-28 14:47) [66]

не пока возникло
:)
В смысле: пока не возникло.


 
Ketmar ©   (2007-02-28 15:46) [67]

> ZeroDivide ©   (28.02.07 14:37) [64]
ты ещё посоветуй анализатор кода для этого привинтить. или вообще эмулятор. %-)


 
Игорь Шевченко ©   (2007-02-28 21:37) [68]

Как и было обещано, вечером попробовал ситуацию:

Стек трассируется из GUI-приложения, если оно запускается не из-под среды. Прошу прощения, вчера из-под нее запускал, поэтому в DbgView ничего не попадало.


 
Alex Konshin ©   (2007-02-28 23:06) [69]

> ZeroDivide ©   (28.02.07 14:37) [64]
> Я не понимаю, каким образом в общем случае можно сделать
> раскрутку стека полагаясь только на call и ret. Да, зачастую
> это будет работать.
>
>
> Доставайте и ложите что хотите, если не трогаете адреса
> возврата. Если трогаете и при exception не возвращаете их
> назад, то лучше отказаться от такого стиля программирования.
>  Если возвращаете их назад, то это уже обрабатываемое исключение.
>
> В общем случае МОЖНО сделать раскрутку стека полагаясь только
> на call и ret... именно так это делает процессор :)

Неправду говоришь. В частном случа можно. В общем нельзя. Я предполагаю, что ты знаешь, как обрабатываются исключения. ret и текущий указатель стека там абсолютно ни при делах и это понятно почему. Между входом в функцию и выходом из нее по ret стек вполне можно менять. Это обычная практика для C/C++. В Pascal такое редкость, потому что нет языковых средств разместить нечто на стеке уже после объявленых переменных или разместить нечто с переменным размером, но даже в VCL есть код который это делает (вроде в Grids.pas). Я в одном проекте, в котором частично портировал сишный код тоже использую этот прием - выделяю память для временных данных переменной длины на стеке, иначе просто получается неэффективно, а там это очень критично. Потом никто не запрещает вызывать сишные DLL и исключение  вполне может возникнуть там, а уж там твой подход просто не сработает, достаточно хотя бы массив с переменными границами определить. И это только для штатных ситуаций. А сколько всего может произойти при нештатных?

Кстати, а куда ты этот обработчик вешаешь? От этого ведь тоже зависит в каком состоянии ты получишь стек и что именно ты раскручиваешь.

Я повторюсь, что твой подход может быть безусловно полезен для получения дополнительной информации. То есть, в идеале нужно их объединить.


 
GrayFace ©   (2007-03-03 11:22) [70]

Piter ©   (27.02.07 19:10) [26]
а, все, понял. В общем, программка печатает стек. Но ведь далеко не все функции помещают аргументы в стек, верно? Если у функции, например, один параметр integer, так он вроде сразу в регистр будет запихнут?

Нет, при кадом call-е в стек пишется адрес возврата, по нему можно определить процедуру. Еще, при создании Stack Frame, есть такой код: push ebp; mov ebp, esp. Он дает возможность скакать по Stack Frame"ам.

novill ©   (28.02.07 13:57) [63]
Перехват делается я так понимаю для того, чтобы первыми получить информацию о прерывании и о его месте, когда стек еще не испорчен самими обработчиками.

Тут главное получить Context прерывания или хотябы epb.

Alex Konshin ©   (28.02.07 23:06) [69]
В частном случа можно. В общем нельзя.

В общем вообще нельзя. Только при чем тут динамическое выделение памяти в стеке? Я не смотрел код ZeroDevide, но, если правильно понял идею, это не должно влиять.


 
Ketmar ©   (2007-03-03 13:06) [71]

> GrayFace ©   (03.03.07 11:22) [70]
mov  eax,esp
push 0x4768d0
push .....
push .....
sub  esp,256

определяй.


 
Piter ©   (2007-03-03 15:05) [72]

GrayFace ©   (03.03.07 11:22) [70]
Нет, при кадом call-е в стек пишется адрес возврата


а как можно понять - адресс возврата это или например аргумент функции?


 
GrayFace ©   (2007-03-04 15:46) [73]

Ketmar ©   (03.03.07 13:06) [71]
Не понял. Речь о том, что по методу ZeroDevide 0x4768d0 будет принят за адрес возврата или о том, что мой способ его проигнорирует, т.к. не создан Stack Frame?

Piter ©   (03.03.07 15:05) [72]
а как можно понять - адресс возврата это или например аргумент функции?

Stack Frame типично создается в начале функции и если отступить от epb на 4 байта попадаем на адрес возврата. Для функций Дельфи (не асмовых) это действует безотказно, но со многими (или с одной, но часто встречающейся) функциями WinAPI этот вокус не проходит.


 
GrayFace ©   (2007-03-04 15:55) [74]

Уточню: для функций Дельфи, в которых создается Stack Frame, это действует безотказно. А для упомянутых функций WinAPI толи epb указывает не на прошлое значение ebp, толи между ebp и адресом возврата что-то есть, в общем, натыкаемя на недопустимый адрес, а почему я не смотрел.


 
ДжекиМайер   (2007-03-11 23:46) [75]

попробывал D6 WIN200

procedure TForm1.FormCreate(Sender: TObject);
begin
//Application.OnException:=AppException;
DebugUtils.HookException(PrintToConsoleAndODS);


procedure TForm1.AppException(Sender: TObject; E: Exception);

begin
 Application.ShowException(E);
 Application.Terminate;
end;


procedure TForm1.Button1Click(Sender: TObject);
var
a:^dword;
begin
a^:=222;


никаких окон - при нажатии на кнопку - приложение как будно сделало halt;

впрочем и юзание стандартного обработчика Application.OnException:=AppException; дает тот же результат!

требуется только перехватить VA, неможет быть реад врите
и выдавать сообщение (стек и коды ошибок ненужны!!!!!!)
незнаю что из исходника выкинуть чтоб остался сам хук
и процедура куда идет сработка при тяжелой ошибке
(не try except )



Страницы: 1 2 вся ветка

Форум: "Прочее";
Текущий архив: 2007.04.08;
Скачать: [xml.tar.bz2];

Наверх





Память: 0.65 MB
Время: 0.047 c
2-1173947665
Alex_C
2007-03-15 11:34
2007.04.08
Компонент для липких окошек


2-1174405501
Lexa11_2002
2007-03-20 18:45
2007.04.08
Как в DLL добавить формы


1-1171298579
Vid0g
2007-02-12 19:42
2007.04.08
Добавление/Извлечение ресурсов в программе


2-1174020682
D@Nger
2007-03-16 07:51
2007.04.08
типизированный файл и класс


2-1174048675
valua
2007-03-16 15:37
2007.04.08
БД и Word





Afrikaans Albanian Arabic Armenian Azerbaijani Basque Belarusian Bulgarian Catalan Chinese (Simplified) Chinese (Traditional) Croatian Czech Danish Dutch English Estonian Filipino Finnish French
Galician Georgian German Greek Haitian Creole Hebrew Hindi Hungarian Icelandic Indonesian Irish Italian Japanese Korean Latvian Lithuanian Macedonian Malay Maltese Norwegian
Persian Polish Portuguese Romanian Russian Serbian Slovak Slovenian Spanish Swahili Swedish Thai Turkish Ukrainian Urdu Vietnamese Welsh Yiddish Bengali Bosnian
Cebuano Esperanto Gujarati Hausa Hmong Igbo Javanese Kannada Khmer Lao Latin Maori Marathi Mongolian Nepali Punjabi Somali Tamil Telugu Yoruba
Zulu
Английский Французский Немецкий Итальянский Португальский Русский Испанский