Kruczki i sztuczki C cz. 1

Gynvael Coldwind

Ten oto poniższy tekst traktuje o kruczkach i sztuczkach w języku C (czystym C). Nie wszystko jest użyteczne, ale mam nadzieje że wszystko (większość) jest ciekawe. Żeby coś zrozumieć wymagana jest jedynie podstawowa wiedza o C. Miłego czytania.
ps. cały poniższy tekst dotyczy procesorów kompatybilnych z Intelem x386 i 32bitowego C

Tablica argumentów
Załóżmy że mamy taką oto funkcję:

int asdf( int a, int b, int c, int d, int e, int f )

I chcemy wypisać po kolei wartości a, b, c, d, e i f na ekran. Można to zrobić w ten sposób:

{
  printf( "%i %i %i %i %i %i", a, b, c, d, e, f );
}

Możemy jednak również użyć tego sposobu (nie twierdze że jest wygodniejszy/szybszy, chodzi po prostu o to że jest taka możliwość):

{
  int *x = &a, i;
  for( i = 0; i < 6; i++ ) printf( "%i ", *x++ );
}

lub

{
  int *x = &a
  printf( "%i %i %i %i %i %i", x[ 0 ], x[ 1 ], x[ 2 ], x[ 3 ], x[ 4 ], x[ 5 ] );
}

We wszystkich trzech przypadkach efekt jest identyczny (pomijam dodatkową spacje w drugim przypadku). Jak te dwa ostatnie sposoby działają? Skąd mamy pewność że parametry są po sobie, jakby w tablicy?
Zrozumieć to pomaga przyjrzenie się kodowi asemblerowemu wygenerowanemu przez kompilator. Wywołanie funkcji asdf wygląda w nim następująco:

push f
push e
push d
push c
push b
push a
call asdf
add esp, 24

Wywnioskować z tego można dwie rzeczy:
a) argumenty funkcji wrzucane są na stos od końca, tj ostatni argument pierwszy, a pierwszy ostatni (push'e)
b) stos (o którym można myśleć jako o tablicy 32bitowych elementów) "rośnie" w kierunku zera, to jest każdy nowy element jest wrzucamy PRZED te już wrzucone (add esp, czyli korekcja stack pointera)
Połączenie tych dwóch informacji daje dość klarowną odpowiedź na nasze pytania (mam nadzieje).

<B>String jako tablica</b>
Część programistów (tych bardziej początkujących) nie wie o dość istotnej rzeczy dotyczącej stringów. Chodzi mi o zapis napisu między cudzysłowami, np. "ala ma kota". Dość dobrym porównaniem jest porównanie tego zapisu do funkcji zwracającej pointer do miejsca w pamięci gdzie jest ów napis umieszczony (i zakończony zerem, w końcu to jest ASCIIZ). Dlatego właśnie wszystkie funkcje które przyjmują pointery jako argument, mogą być wywoływane ze stałym napisem. Przykładem może być choćby:

  fwrite( "ala ma kota", 1, 11, stdout );

Czy daje to jakieś dodatkowe możliwości ? Tak.
Skoro jest to funkcja zwracająca pointer, to można jej używać jak tablicy, to jest z indeksem w nawiasach kwadratowych. Np.

  putchar( "ala ma kota"[ 3 ] ); // wypisze na ekranie spacje

Możemy to zapisać równie dobrze dodając index do pointera i używając dereferencji:

  putchar( *("ala ma kota" + 3) );

Dodawanie oczywiście jest przemienne, więc można to zapisać jako:

  putchar( *(3 + "ala ma kota" ) );

A idąc o krok dalej, można wrócić do zapisu z indeksem:

  putchar( 3["ala ma kota"] );

<B>Porównywanie napisów</b>
Początkujący programiści szybko przekonują się że w C porównywanie napisów nie jest równie łatwe jak w Pascalu. Nie wystarczy napisać if( "asdf" == buf ). Zazwyczaj do porównań wykorzystuje się funkcje strcmp itp., rzadziej memcmp.
Oczywiście wywołanie takiej funkcji oraz analiza po jednym charze trochę czasu zajmuje. Dla napisów które mają określoną długość, np. 4, można jednak zrobić tak:

char *a = "asdf", *b = "fdsa";
if( *(int*)a == *(int*)b ) { ... }

Dlaczego jest to możliwe? Ponieważ inty mają 32 bity, czyli cztery bajty, dokładnie tyle ile nasz napis. Jako że w pamięć nie ma określonych typów, jest to po prostu wielka tablica bajtów, nie ma różnicy dla komputera czy porównujemy coś co uznajemy za litery czy coś co uznajemy za liczby. A porównanie naraz X znaków jest oczywiście o wiele szybsze niż porównywanie X znaków pojedynczo.
Do porównań można wykorzystać dowolny podstawowy typ zmiennych (char, short int, int, long int, float, double, long double, etc), warunkiem jest jedynie stała ilość znaków.

<B>int i jego zapis w pamięci</b>
W sumie ta "sztuczka" nie dotyczy tylko intów, ale wszystkich typów wielobajtowych danych, i jest ściśle związana z procesorami Little-Endian.
Załóżmy że mamy zmienną unsigned int asdf = 0x12345678;. Wykonanie poniższego programu pokaże zapis czterech kolejnych bajtów w pamięci spod adresu zmiennej asdf, czyli jak ona wygląda (jest zapiana) w pamięci. Logicznie rzecz biorąc powinien być on identyczny z faktyczną liczbą.

#include<stdio.h>
int main( void )
{
  unsigned int asdf = 

0x

12345678;
  unsigned char *x = (unsigned char*)&asdf
  printf( "0x%2x%2x%2x%2x %p\n", x[ 0 ], x[ 1 ], x[ 2 ], x[ 3 ], (void*)asdf );
  return 0;
}

Okazuje się jednak że program pokazał dwie różne liczby:
0x78563412 0x12345678
Czyżby pod tym adresem w pamięci było coś zupełnie innego? Okazuje się że nie. Łatwo zauważyć że liczby są podobne, a dokładniej rzecz biorąc jest tylko inna kolejność bajtów. Identyczne liczby będą więc wyświetlone gdy zmienimy kolejne indeksy x z 0 1 2 3 na 3 2 1 0. To już wiemy jak są te liczby zapisywane, od tyłu bajtami. Co nam to jednak daje? Na pewno jedną rzecz utrudnia odrobinę, mianowicie pisanie aplikacji które mają działać zarówno pod systemami Little-Endianowymi jak i Big-Endianowymi. Łatwiej dzięki temu natomiast pobrać najmniej znaczący bajt liczby.

unsigned int asdf = 

0x

12345678;
unsigned char x = *(unsigned char*)&asdf

<B>Napis czy funkcja?</b>
Czym jest funkcja? Podprogramem, fragmentem kodu. A czym jest funkcja po skompilowaniu? Tym samym, podprogramem, skompilowanymi rozkazami umieszczonymi w pamięci. Czym w takim razie jest wywołanie funkcji? Skokiem pod podany adres.
Załóżmy w takim razie że mamy jakąś funkcje, która nie robi nic, nie przyjmuje argumentów i nic nie zwraca. Po skompilowaniu będzie ona miała np. postać:
90 // nop - brak operacji
C3 // ret - powrót
Załóżmy teraz że chcemy w jakimś celu wykonać tą funkcje w programie pisanym w C. Jak to zrobić? Np. w poniższy sposób:

((void(*)(void))"\x90\xc3")( );

lub

unsigned short asdf = 0xc390;
((void(*)(void))&asdf)( );

Przykładowe użycie:
tak wygląda funkcja przed skompilowaniem (w sumie jest to int asdf( int a ) { return a+1; }):

pop edx
pop eax
inc eax
sub esp, 4
push edx
ret

a tak program:

#include<stdio.h>

int main( void )
{
  int a = 12;
  printf( "%i %i\n", a, 
  ((int(*)(int))"\x5a\x58\x40\x81\xec\x04\0\0\0\x52\xc3")( a ) );
  return 0;
}

W sumie na tyle na dzisiaj.

5 komentarzy

kiedy część druga?! :D

"Tablica argumentów" działa tylko przy stdcall (który jest domyślny). Jak podamy fastcall to wiadomo dlaczego nie zadziała.

  1. putchar( 3["ala ma kota"] ); tego nie znałem :)
  2. masz jakieś wałki z tagiem code - spróbuj to poprawić

eeeeeee :) spoko

2 - anom, poprawilem ;>