2006.07_Jądro systemu operacyjnego _[Inzynieria Oprogramowania].pdf
(
491 KB
)
Pobierz
441663947 UNPDF
Inżynieria
oprogramowania
Grzegorz Pełechaty
Jądro systemu operacyjnego
mów operacyjnych stanowi z całą pewno-
ścią jedną z bardziej rozwojowych dziedzin
informatyki. Na rynku pojawiają się nowe typy pro-
cesorów, które oferują coraz to bardziej zaawanso-
wane możliwości. Obecnie największą popularno-
ścią wśród zastosowań domowych cieszą się pro-
cesory z rodziny x86. Ich rozwój jest ściśle powią-
zany z rosnącymi wymaganiami użytkowników.
Pierwsze procesory 80186 działały jedynie w try-
bie rzeczywistym (ang.
real mode/real address mo-
de
), który ograniczał się do możliwości adresowa-
nia megabajta pamięci operacyjnej (20 bitowa szy-
na adresowa). Począwszy od procesora 80286 te
restrykcyjne ograniczenia powoli zmniejszały się,
i tak w procesorze 80286 po raz pierwszy wprowa-
dzono tryb chroniony (ang.
protected mode
) oraz
umożliwiono adresowanie 16 megabajtów pamięci
(24 bitowa szyna adresowa, ale procesor pozostał
nadal 16 bitowy). Jednak prawdziwe zmiany wniósł
procesor 386DX, który w pewnym sensie zrewo-
lucjonizował rynek komputerowy, a wykorzystane
w nim rozwiązania są powszechnie stosowane do
dziś. Główną zaletą tego procesora było wprowa-
dzenie możliwości 32-bitowego, chronionego trybu
pracy. Umożliwiało to adresowanie do 4GB pamię-
ci. 32-bitowy tryb chroniony wniósł szereg innych
udogodnień. Stary sposób zarządzania pamięcią
poprzez segmentację został wyparty przez stron-
nicowanie (ang.
paging
), które jest bardziej dosto-
sowane do wymagań programisty oraz nie zawie-
ra tylu ograniczeń co segmentacja (chodzi głównie
o limit wpisów w lokalnej tablicy deskryptorów).
Jednym z ważniejszych udogodnień, jakie wpro-
wadził tryb chroniony jest możliwość tworzenia wie-
lu procesów, z których każdy wykonuje pewien pro-
gram. W architekturze x86 do tych celów stworzono
specjalne segmenty w globalnej tablicy deskrypto-
rów, które określają stan każdego procesu (ang.
Task
State Segment
). Wszystkie te rozwiązania są jednak
bezużyteczne bez odpowiedniego programu, któ-
ry mógłby je rozsądnie wykorzystać, przez co praca
z komputerem stała by się prostsza i mniej zawod-
na. W tym miejscu pojawia się miejsce na system
operacyjny, który jest programem mającym za za-
danie zarządzanie sprzętem i udostępnianie w pro-
stej formie zestawu funkcji, dzięki którym przeciętny
człowiek odnajdzie się binarnym świecie.
Zarządzanie pamięcią
w trybie rzeczywistym
Tak jak już wspomniałem, w trybie rzeczywistym mo-
żemy zaadresować jedynie 1MB pamięci, a całe za-
rządzanie tym obszarem sprowadza się do odgórne-
go podzielenia go na segmenty. Do dyspozycji pro-
gramisty oddano 64K segmentów, każdy segment
zaczyna się co 16-ty bajt. Jako że limit segmentu wy-
nosi 64KB, muszą one nachodzić na siebie. Takie
rozwiązanie umożliwia adresowanie każdej komórki
pamięci na wiele sposobów. W trybie rzeczywistym
adresowanie odbywa się zawsze poprzez podanie
dwóch 16bitowych wartości: numeru segmentu oraz
przesunięcia w nim. Aby obliczyć adres liniowy nale-
ży użyć wzoru: segment*16+przesunięcie.
Przerwania w trybie rzeczywistym
W trybie rzeczywistym mamy do dyspozycji 256 prze-
rwań. Wliczamy w to przerwania programowe oraz
sprzętowe. Przerwania programowe, jak sama na-
zwa wskazuje dają nam możliwość zaprogramowania
się, czyli ustalenia gdzie procesor ma przeskoczyć po
wywołaniu instrukcji
INT.
Przerwania sprzętowe – IRQ
różnią się tylko tym, że oprócz możliwości wywołania
ich bezpośrednio z kodu programu, mogą być również
wywołane przez izyczne urządzenie. Każda linia IRQ
jest przypisana do innego wektora przerwań w IVT
(ang.
Interrupt Vectors Table
).
IVT jest tablicą, która zawiera 256 wpisów typu
segment:offset. Każdy taki wpis jest nazywany wek-
torem przerwania. Kiedy procesor otrzymuje polece-
nie wykonania przerwania, musi znać nowy CS i IP
punktu wejścia programu obsługi.
Wartości te pobiera z IVT wykonując następują-
ce obliczenia:
Autor jest od 7 lat programistą języka C. Interesuje się
zagadnieniami systemów operacyjnych, elektroniką i sie-
ciami neuronowymi. Obecnie pracuje nad projektem dar-
mowego systemu sieciowego, opartego o jądro monoli-
tyczne, oraz w pełni zgodnego ze standardami POSIX
(
http://www.netcorelabs.org
). System jest rozpowszech-
niany na warunkach licencji General Public License v2.
Kontakt z autorem:
grzegorz.pelechaty@areoos.com
•
Segment=IVT[int*4]
•
Offset=IVT[int*4+2]
Tak więc jeden wektor zajmuje w IVT dokładnie 4baj-
ty. Przed przejściem do programu obsługi przerwa-
nia, procesor odkłada na stos następujące rejestry:
•
SS
- segment stosu,
•
SP
– obecne przesunięcie w segmencie stosu,
48
www.sdjournal.org Software Developer’s Journal 7/2006
P
rojektowanie oraz programowanie syste-
Programowanie systemów operacyjnych
Mapa pierwszego megabajtu pamięci
Listing 1.
Struktura deskryptora segmentu w Globalnej
Tablicy Deskryptorów
•
00000000 – 000003FF
Tablica wektorów przerwań
•
00000400 – 000004FF
Obszar danych biosu
•
00000500 – 0009FBFF
Pamięć konwencjonalna (640KB)
•
00007C00 – 00007DFF
Program rozruchowy
•
0009FC00 – 0009FFFF
Rozszerzony obszar danych biosu
(EBDA)
•
000A0000 – 000BFFFF
Pamięć VGA (128KB)
•
000A0000 – 000AFFFF
Bufor ramki VGA(64KB)
•
000B0000 – 000B7FFF
Pamięć dla kart monochromatycznych
(32KB)
•
000B8000 – 000BFFFF
Pamięć dla kart kolorowych (32KB)
•
000C0000 – 000C7FFF
BIOS karty graicznej (32KB – ROM)
•
000F0000 – 000FFFFF
BIOS płyty głównej (64KB – ROM)
struct
gdt_seg_desc
{
unsigned
short
len15_0
;
unsigned
short
base15_0
;
unsgined
char
base23_16
;
unsigned
char
lags1
;
unsigned
char
lags2
;
unsigned
char
base31_24
;
}
;
Zarządzanie pamięcią
w trybie chronionym
W trybie chronionym istnieją dwa mechanizmy zarządzania
pamięcią. Poprzez segmentację oraz stronicowanie. Naj-
pierw postaram się przybliżyć pojęcie segmentacji, ponie-
waż wygląda ona trochę inaczej, niż miało to miejsce w try-
bie rzeczywistym.
Znana z trybu rzeczywistego zamiana zawartości reje-
strów segmentowych i przesunięcia na adres izyczny tra-
ci sens w trybie chronionym. Tutaj segmenty są od sie-
bie odseparowane i chociaż nadal są dostępne programo-
wo, interpretacja ich zawartości jest zupełnie inna. Rejestr
segmentowy przechowuje teraz selektor segmentu, a nie
wprost jego adres. 13 najstarszych bitów tego rejestru sta-
nowi adres 8bajtowej struktury opisującej dany segment
(ang.
Segment Descriptor
). Z pozostałych trzech bitów dwa
poświęcone zostały na implementację czteropoziomowe-
go systemu praw dostępu do segmentu, a jeden określa
czy wspomniany powyżej adres odnosi się do tzw.
tablicy
lokalnej
czy
globalnej
. Rekordami w tych tablicach są wła-
śnie deskryptory segmentów. Każdy z nich zawiera jedno-
znaczną informację o lokalizacji segmentu w pamięci i jego
rozmiarach. W ten sposób zdeiniowany jest spójny obszar
o adresie początkowym wyznaczonym przez liczbę 32-bi-
tową. Na liczbę określającą rozmiar takiego bloku przezna-
czone zostało pole 20-bitowe. Istnieją dwie możliwości in-
•
FLAGS
– lagi procesora,
•
CS
- segment kodu,
•
IP
– obecne przesunięcie w segmencie kodu (licznik in-
strukcji).
Rejestry są odkładane po to, aby po powrocie z przerwa-
nia, program mógł dalej kontynuować swoje działanie. Stos
wraca do poprzedniej wartości, ponieważ dane odłożone
na nim przez program obsługi przerwania są teraz bezu-
żyteczne.
Tryb chroniony i pierścienie ochrony
Pierścienie ochrony (ang.
Protection rings
) są to pozio-
my uprzywilejowania, jakie zastosowano w procesorach IA-
286p+. System uprawnień jest dosyć rozbudowany i opiera
się o czteropoziomowy układ zabezpieczeń, w którym pier-
ścień zerowy jest najbardziej uprzywilejowany, a trzeci posia-
da znaczne ograniczenia. Uprawnienia te obowiązują w pra-
wie wszystkich elementach trybu chronionego. Jedynym wy-
jątkiem jest stronicowanie, które będzie dokładniej omówione
w kolejnej części cyklu.
Spis wektorów linii IRQ w trybie
rzeczywistym
Listing 2.
Funkcja tworząca nowy segment w Globalnej
Tablicy Deskryptorów
• Linia IRQ Wektor Urządzenie generujące syngnał IRQ
•
0 08h
Zegar systemowy
•
1 09h
Klawiatura
•
2 0Ah
Wyjście kaskadowe do układu Slave
•
3 0Bh
Port COM2
•
4 0Ch
Port COM1
•
5 0Dh
Port LPT2
•
6 0Eh
Kontroler napędu dysków elastycznych
•
7 0Fh
Port LPT1
•
8 70h
Zegar czasu rzeczywistego (RTC)
•
9 71h
Wywołuje przerwanie IRQ2
•
10 72h
Zarezerwowane
•
11 73h
Zarezerwowane
•
12 74h
Zarezerwowane
•
13 75h
Koprocesor arytmetyczny
•
14 76h
Kontroler dysku twardego
•
15 77h
Zarezerwowane
struct
gdt_seg_desc
*
gdt_table
=
(
struct
gdt_seg_desc
)
GDT_ADDRESS
;
void
createSegment
(
int
pos
,
unsigned
long
base
,
unsigned
long
len
,
unigned
char
lags1
,
unsigned
char
lags2
)
{
gdt_table
[
pos
]
.
len15_0
=
(
unsigned
short
)(
len
&
0xFFFF
);
gdt_table
[
pos
]
.
base15_0
=
(
unssigned
short
)(
base
&
0xFFFF
);
gdt_table
[
pos
]
.
base23_16
=
(
unsigned
char
)(
(
base
>>
16
)
&
0xFF
);
gdt_table
[
pos
]
.
lags1
=
lags1
;
gdt_table
[
pos
]
.
lags2
=
lags2
|
((
len
>>
16
)
&
0xf
);
gdt_table
[
pos
]
.
base31_24
=
(
unsigned
char
)(
(
base
&
0xF000
)
>>
24
);
}
Software Developer’s Journal 7/2006
www.sdjournal.org
49
Inżynieria
oprogramowania
Listing 3.
Deinicje poszczególnych bitów we lagach
deskryptora segmentu
bitowy, budowany jest ze złożenia zawartości 16bitowe-
go rejestru segmentowego i 32bitowego rejestru przesu-
nięcia. W przypadku ziarnistości 4KB maksymalny roz-
miar segmentu wynosi 4GB. Liczba możliwych segmen-
tów to 2^14(2^13 deskryptorów lokalnych i tyle samo glo-
balnych), co daje w sumie astronomiczną objętość 64TB
(2^14*2^32). Właściwie już jeden taki segment stanowi
wielkość optymalną – 4GB przestrzeni adresowej zaspo-
kaja przy obecnym rozwoju techniki PC dość wygórowa-
ne wymagania. Rozwiązanie takie, określane jako “płaski
model pamięci” (ang.
lat memory model
), stosowane jest
w systemie Windows NT.
Opis
deinicji
poszczeg
ó
lnych
lag
segmentu
:
// FLAGS1 (P + DPL + SYS/APP + TYPE)
#deine GDT_PRESENT 0x80
#deine GDT_DPL3 0x60
#deine GDT_DPL1 0x20
#deine GDT_DPL2 0x40
#deine GDT_DPL0 0x00
// GDT_SYS będzie poruszone podczas omawiania
// wielozadaniowości. Obecnie interesuje nas
// tylko GDT_APP, które przeznaczone jest dla segmentu
// danych lub kodu.
#deine GDT_SYS 0x00
#deine GDT_APP 0x10
Zarządzanie
Globalną Tablicą Deskryptorów
Listing 2 zawiera funkcję, która tworzy nowy segment w
GDT. Struktura opisująca pojedynczy deskryptor segmen-
tu znajduje się na Listingu 1. Ustalmy jeszcze raz jak obli-
czyć selektor danego segmentu. Na selektor składa się ad-
res deskryptora względem początku Globalnej Tablicy De-
skryptorów oraz poziom uprzywilejowania i informacja o
tym czy segment znajduje się w
tablicy lokalnej
, czy
global-
nej
.
// Dodatkowe lagi dla segmentów innych niż data lub
// code (GDT_SYS)
#deine GDT_RESERVED 0x0
#
deine
GDT_TSS16
0x1
// 0001 16 bitowy TSS (dostępny)
#
deine
GDT_LDT
0x2
// 0010 LDT
#
deine
GDT_TSS16_BUSY
0x3
// 0011 16 bitowy TSS (zajęty)
#
deine
GDT_CALL16
0x4
// 0100 16 bitowa bramka wywołań
#
deine
GDT_TASK
0x5
// 0101 Bramka zadania
#
deine
GDT_INT16
0x6
// 0110 16 bitowa bramka
// przerwania
#
deine
GDT_TRAP16
0x7
// 0111 16 bitowa bramka pułapki
#
deine
GDT_TSS32
0x9
// 1001 32 bitowy TSS (dostępny)
#
deine
GDT_TSS32_BUSY
0xB
// 1011 32 bitowy TSS (zajęty)
#
deine
GDT_CALL32
0xC
// 1100 32 bitowa bramka wywołań
#
deine
GDT_INT32
0xE
// 1110 32 bitowa bramka
// przerwania
#
deine
GDT_TASK_GATE
0xF
// 1111 32 bitowa bramka pułapki
// Dla GDT_APP
#deine GDT_DATA 0x00
#deine GDT_WRITE 0x02
#deine GDT_EXP_DOWN 0x04
#deine GDT_CODE 0x08
#deine GDT_READ 0x02
#deine GDT_CONF 0x04
selector=numer_segmentu*sizeof(struct gdt_desc)+DPL+ (
4 –- jeżeli deskryptor jest w LDT)
Sama tablica GDT jest opisana specjalnym, 48bitowym de-
skryptorem. Struktura opisująca ten deskryptor wygląda na-
stępująco:
struct gdt_desc {
unsigned short gdt_size;
unsigned long gdt_address;
} __atribute__((packed));
Pierwsze 16 bajtów powinno zawierać rozmiar tablicy GDT mi-
nus 1bajt, czyli 8192*8-1.
Tablicę ładujemy poleceniem
lgdt
, które przyjmuje izycz-
ny adres deskryptora w pamięci (w postaci bezpośredniego
adresu, bądź też rejestru). Musimy pamiętać, że pierwszy de-
skryptor jest zawsze pusty. Nie ma możliwości, aby został on
wykorzystany w jakikolwiek sposób przez system.
// FLAGS2 (G + D/B + 0 + AVL)
// Ziarnistość danego segmentu
#deine GDT_GRANULARITY 0x80
// Rodzaj segmentu – 32 lub 16 bitowy
#deine GDT_USE32 0x40
#deine GDT_USE16 0x00
// Segment do dowolnego użytku przez system
#
deine
GDT_AVAIL
0x00
Jądro systemu
Teraz spróbujemy zebrać te wszystkie informacje w spójną
całość i napiszemy proste jądro systemu operacyjnego. Nie
będzie to oczywiście jądro, które byłoby w stanie zrobić co-
kolwiek, ale po uruchomieniu zobaczymy napis
hello world
i to
powinno nam na razie wystarczyć.
Listing 4.
Nagłówek multiboot dla wykonywalnych plików
ELF
terpretowania liczby w tym polu. W trybie 1:1 (ziarnistość
1B) rozmiar maksymalny wynosi po prostu 2^20 = 1MB.
Gdyby jednak przyjąć jednostkę 4KB (ziarnistość 4KB), roz-
miar segmentu może sięgać do 2^20*2^12 = 2^32 = 4GB.
Informacja o tym, która z konwencji aktualnie obowiązuje,
zawarta jest w deskryptorze.
Adres logiczny, do którego odwołuje się procesor 32-
struct
multiboot_header
{
unsigned
long
magic
;
unsigned
long
lags
;
unsigned
long
checksum
;
}
;
50
www.sdjournal.org Software Developer’s Journal 7/2006
Programowanie systemów operacyjnych
Listing 5.
Kod inicjalizacyjny jądra
.text
.globl
_start
_start
:
jmp
multiboot_entry
.align 4
multiboot_header
:
.
long
0x1BADB002
.
long
0x00000003
.
long
-
(
0x1BADB002 + 0x00000003
)
multiboot_entry
:
movl
$
(
stack
+100
)
,%
esp
call
setup_gdt
call
__main
mbi
:
.
long
0x0
setup_gdt
:
movl
$
gdt_table
, %
esi
movl
$0xA000, %
edi
movl
$8, %
ecx
rep
movsl
movl
$0xA000+8*8, %
edi
movl
$0x2000-8, %
ecx
ill_gdt
:
movl
$0,
(
%
edi
)
movl
$0, 4
(
%
edi
)
addl
$8, %
edi
dec
%
ecx
jne
ill_gdt
1:
lgdt
gdt
ljmp
$
(
0x10
)
, $
go
go
:
movl
$
(
0x18
)
, %
eax
movl
%
eax
, %
ds
movl
%
eax
, %
es
movl
%
eax
, %
fs
movl
%
eax
, %
gs
movl
%
eax
, %
ss
ret
.data
gdt
: .
word
0x2000*8-1
.
long
0xA000
gdt_table
:
.
quad
0x0000000000000000 #
pusty
deskryptor
.
quad
0x0000000000000000 #
nie
u
ż
ywamy
.
quad
0x00cf9a000000ffff # 0x10
kernel
4
GB
code
at
0x00000000
.
quad
0x00cf92000000ffff # 0x18
kernel
4
GB
data
at
0x00000000
.comm
stack
, 0x500
Zestaw Narzędzi
Do rozpoczęcia prac nad pisaniem własnego systemu ope-
racyjnego niezbędny będzie nam pewien zestaw narzędzi,
który w znacznej mierze ułatwi nam to zadanie:
języka wysokiego poziomu, musimy również posiadać kompi-
lator asemblera. Szereg programów z pakietu
binutils
pomo-
że nam w skonsolidowaniu całego obrazu. Użycie emulatora
PC jest wysoce wskazane, ponieważ dzięki niemu nie będzie-
my zmuszeni za każdym razem restartować komputera w ce-
lu sprawdzenia poprawności naszego kodu. Ostatnim progra-
mem jaki musimy posiadać jest program rozruchowy zgodny
ze standardem multiboot. Przykładem takiego programu jest
GRUB, który staje się coraz bardziej powszechny. Oczywiście
możemy napisać własny program rozruchowy, jednak mija się
to z celem. GRUB zagwarantuje nam zgodność z większością
sprzętu oraz przejdzie za nas w tryb chroniony tym samym
oddając w nasze ręce w pełni 32-bitowe środowisko pracy.
Wszystkie wymienione powyżej narzędzia są rozpowszech-
• edytor tekstu
• GCC & GAS
• GNU binutils (ld, make)
• opcjonalnie emulator (qemu/vmware)
• program rozruchowy zgodny ze standardem multiboot
Edytor tekstu będzie nam oczywiście potrzebny do pisania ko-
du. Kompilatory
gcc i gas
posłużą do jego skompilowania. Po-
nieważ nie jest możliwe napisanie jądra jedynie przy użyciu
R E K L A M A
Inżynieria
oprogramowania
Listing 6.
Przykładowe jądro systemu, wypisujące napis
“hello world” w lewym górnym rogu ekranu
ustawiony na 4GB, a co za tym idzie ziarnistości segmentu mu-
si wynosić 4KB. Segment ten ma uprawnienia pierścienia 0. Ko-
lejny segment jest przeznaczony na dane. Ma on taki sam adres
bazowy i limit jak segment kodu, jednak różni się typem. Po wy-
pełnieniu GDT, wywołujemy funkcję
_ main()
, która będzie po-
czątkiem naszego właściwego jądra.
static
char
*
video
=(
char
*)
0xB8000
;
void
clrscr
(
void
)
{
int
i
;
for
(
i
=
0
;
i
<
80
*
50
;
i
+=
2
)
{
video
[
i
]=
32
;
video
[
i
+
1
]=
0x7
;
}
}
void
puts
(
char
*
msg
)
{
char
*
ptr
=
video
;
while
(*
msg
)
{
*
ptr
++=*
msg
++;
*
ptr
++=
0x7
;
}
}
void
__main
(
void
)
{
clrscr
();
puts
(
"hello world!"
);
for
(;;);
}
Kernel
Na razie nasze jądro nie będzie zbytnio rozbudowane. Napi-
szemy prostą funkcję czyszczącą ekran w tekstowym trybie
VGA oraz funkcję wypisującą ciąg znaków w lewym górnym
rogu ekranu
Jak widać na Listingu 6, po wypisaniu komunikatu koń-
czymy funkcję nieskończoną pętlą. Jest to jedyne wyjście po-
nieważ nie mamy systemu do którego moglibyśmy powrócić.
Gdybyśmy jednak kontynuowali działanie, licznik instrukcji (re-
jestr EIP) wskazywałby na pamięć, nie zawierającą instruk-
cji procesora, co spowodowałoby wywołanie 6 wyjątku. Pro-
cesor próbując wywołać przerwanie nr 6 natraiłby na kolejny
problem, ponieważ nie mamy załadowanej tablicy IDT (ang.
Interrupt Descriptors Table
). Wtedy wystąpiłby potrójny błąd
(ang.
Triple fault
), który wiąże się z natychmiastowym restar-
tem procesora.
niane na warunkach
General Public License
, więc są całkowi-
cie darmowe.
Kompilacja
Przy kompilacji tak dużych projektów, jakimi są systemy ope-
racyjne bardzo dobrym rozwiązaniem jest zastosowanie pli-
ków
mak
e. N a Listingu 7 przedstawiono sposób użycia tych
plików, w oparciu o źródłowe pliki, które stworzyliśmy wcze-
śniej. Mowa o kodzie inicjacyjnym, który powinniśmy zapi-
sać w pliku
init.S
oraz źródle
jądra, które powinno nosić na-
zwę
main.c
Listing 7 zapisujemy pod nazwą
makeile
w katalogu ze
źródłami.
Kod inicjacyjny jądra
Aby GRUB mógł załadować jądro, musi ono posiadać specjalny
nagłówek informacyjny. Adres tego nagłówek musi być wyrów-
nany do czterech bajtów. Struktura opisująca nagłówek multi-
boot dla wykonywalnych plików ELF znajduje się na Listingu 4.
Standardowe wartości jakie powinny być użyte w naszym
wypadku to:
Listing 7.
Plik makeile dla jądra
magic=0x1BADB002
lags=0x00000003
checksum=-(0x1BADB002 + 0x00000003)
CC=gcc
LD=ld
OBJS=init.o main.o
Na Listingu 5 wypełniamy GDT czterema deskryptorami. De-
skryptor numer 2 wskazuje na segment kodu, którego limit jest
CFLAGS = -fno-builtin -nostdlib -nostdinc -Wno-main -O2
all: $(OBJS)
$(LD) -Tkernel.lds -S -X -o kernel --start-group $(OBJS)
--end-group
.c.o:
$(CC) $(CFLAGS) -c $
<
-o $@
.S.o:
$(CC) $(CFLAGS) -traditional -c
$
<
-o $@
Rysunek 1.
Jądro systemu uruchomione pod emulatorem
Vmware
CLEAN_FILES = $(OBJS)
clean:
rm -rf $(CLEAN_FILES)
dep:
ind . -name
'*.c'
-o -name
'*.S'
|xargs gcc -M $(CFLAGS)
>
.depend
ifeq (.depend,$(wildcard .depend))
include .depend
endif
52
www.sdjournal.org Software Developer’s Journal 7/2006
Plik z chomika:
Kapy97
Inne pliki z tego folderu:
2008.02_Extreme Programming i CMMI – kreatywność czy dyscyplina (Cz. 3)_[Inzynieria Oprogramowania].pdf
(398 KB)
2007.05_Mechanizm koncepcji w języku C++ nowe oblicze szablonów_[Inzynieria Oprogramowania].pdf
(556 KB)
2006.10_Przegląd modeli cyklu życia oprogramowania_[Inzynieria Oprogramowania].pdf
(470 KB)
2006.10_Łączenie kodu C++ z zarządzanym kodem .NET_[Inzynieria Oprogramowania].pdf
(467 KB)
2006.09_Data Protection API i .NET Framework 2.0_[Inzynieria Oprogramowania].pdf
(434 KB)
Inne foldery tego chomika:
Algorytmy
Antyhaking
Aplikacje Biznesowe
Aspekty
Bazy Danych
Zgłoś jeśli
naruszono regulamin