- Nizkonavnostni jeziki se približujejo strojna oprema ponuja maksimalen nadzor in minimalno abstrakcijo.
- C velja za jezik srednje ravni, vendar pri uporabi s sistemskim pristopom deluje kot jezik nizke ravni.
- Binarna koda, strojni jezik in zbirni jezik tvorijo temelj, na katerem so zgrajeni C in drugi jeziki.
- Ti jeziki so idealni za OS, vozniki, visokozmogljiva vgrajena vdelana programska oprema in programska oprema.
Ko govorite o tem primeri nizkonivojskega C Ne gre le za ogled štirih vrstic kode, temveč za razumevanje, kako ta koda komunicira skoraj iz oči v oči s strojno opremo. Strojni jeziki pogosto vzbujajo strahospoštovanje, so pa tudi temelj operacijskih sistemov, gonilnikov in veliko kritične programske opreme, ki jo uporabljamo vsak dan, ne da bi se tega sploh zavedali.
V naslednjih vrsticah boste našli podrobno razlago o Kaj so nizkonivojski jeziki in kako se C umešča kot strojno usmerjen srednjenivojski jezik?Kakšno vlogo igrajo strojna koda, binarna koda in asemblerski program ter zakaj je vse to ključnega pomena, ko razmišljate o programiranju »od začetka«? Ideja je, da na koncu dobite jasno, praktično in utemeljeno vizijo brez nepotrebnih olepševanj.
Kaj točno je nizkonivojski jezik?
Nizkonavnostni jezik je tisti, katerega Navodila so zelo podobna dejanskemu delovanju procesorja in ponujajo zelo malo abstrakcije glede sistemske arhitekture. Z drugimi besedami, koda, ki jo napišete, je zelo blizu tistemu, kar CPU razume: preproste operacije z registri, pomnilnikom in skoki pri izvajanju.
Ta bližina strojne opreme pomeni, da so ti jeziki ni zelo prenosljiv in zelo odvisen od notranje zasnove mikroprocesorjaIsti program v strojnem jeziku za določen procesor ne bo deloval tako kot za drugo arhitekturo, ker se nabor ukazov in načini naslavljanja spremenijo.
Izraz "nizki" ne pomeni, da so manj zmogljivi jeziki; ravno nasprotno, Zagotavljajo maksimalen nadzor nad delovanjem strojaTako se imenujejo, ker je razdalja (abstrakcija) med človeškim jezikom in jezikom, ki ga izvaja CPU, zelo majhna.
V praksi, ko govorimo o nizkonivojskih jezikih, pomislimo predvsem na binarna koda (strojni jezik) in asemblerKljub temu so jeziki, kot je C, pogosto opisani kot srednjenivojski, ker združujejo zelo neposreden dostop do strojne opreme z nekaj strukturirane abstrakcije.
Razvrstitev po ravneh: od strojnih do srednjih jezikov
Jeziki programiranje Naročiti jih je mogoče približno od od nižje do višje ravni abstrakcije glede na strojno opremoTa klasifikacija pomaga določiti, kam spada C in zakaj velja za programski jezik na pol poti med povsem nizkonivojskimi in visokonivojskimi programskimi jeziki.
Strojni jezik (1GL)
Strojni jezik, imenovan tudi jezik prve generacije ali 1GLJe edini, ki ga CPU izvorno razume. Sestavljen je v celoti iz bitnih zaporedij (enic in ničel), ki predstavljajo tako navodila kot podatke, kodirane v skladu s specifično arhitekturo procesorja.
Če odprete izvedljivo datoteko v urejevalniku navadnega besedila, boste videli, kako izgleda. nesmiselni znaki, mnogi od njih niso natisljiviZa tem navideznim kaosom se skrivajo binarni vzorci, ki jih centralna enota interpretira kot navodila za dodajanje, premikanje podatkov, skok na drugo pomnilniško lokacijo itd.
Ukazi strojnega jezika običajno zasedejo eno ali več pomnilniških lokacij: En del ustreza operacijski kodi (opcode), drugi pa operandom. (pomnilniški naslovi, registri ali takojšnje vrednosti). Natančna oblika je odvisna od načinov naslavljanja, ki jih je določil proizvajalec procesorja.
Na primer, v a Mikroprocesor Z80 Morda bomo videli nekaj takega:
| Indeks pomnilnika | Binario | Šestnajstiški | Pomen |
|---|---|---|---|
| 0 | 10000111 | 87 | Ukaz za dodajanje vsebine naslednjega položaja v akumulator |
| 1 | 01110111 | 77 | Podatki: številska vrednost 119 v desetiškem sistemu (77 v šestnajstiškem sistemu). |
Delo z neposrednim pisanjem teh binarnih vzorcev je zelo dolgočasno in nagnjeno k človeškim napakamZato je redko, da bi kdo programiral ročno v čistem strojnem jeziku, razen v izobraževalnih ali izjemno specializiranih primerih.
Binarna koda in strojni jezik
O binarni kodi se pogosto razpravlja, kot da bi bila nekaj ločenega od strojnega jezika, vendar v bistvu Sta dve plati istega kovancaBinarni zapis je preprosto bitna predstavitev navodil in podatkov, ki jih procesor razume.
Binarni sistem je sestavljen samo iz 0 in 1, kjer 1 je običajno povezana z aktivnim stanjem, 0 pa z neaktivnim stanjem.Na tej preprosti osnovi so zgrajene vse računalniške operacije. Vsaka kombinacija bitov ima natančen pomen, ki ga določa arhitektura procesorja.
Čeprav kot programer običajno ne pišeš nizov ničel in enic neposredno, Vsa programska oprema se sčasoma prevede v to obliko pred izvedbo, bodisi s predkompilacijo bodisi s prevajanjem v realnem času.
Zbiralni jezik (2GL)
Zbiralni jezik se šteje za jezik druge generacije ali 2GLPredstavlja majhen korak v abstrakciji v primerjavi s strojno kodo: namesto pisanja bitov uporabljate mnemotehnike (skrajšane besede) za vsak ukaz in simboli za predstavitev naslovov, konstant ali oznak.
Vsak ukaz v zbirniku običajno skoraj ena proti ena ustrezajo strojnemu ukazuNa primer, za premikanje vrednosti iz enega registra v drugega ali iz pomnilniške lokacije v register se uporabljajo mnemoniki, kot je MOV, in če dodamo, se uporablja ADDZa skok z ene točke v programu na drugo se uporabljajo ukazi, kot so JMP.
Asemblerski program je človeku berljivo besedilo, vendar CPU ga ne more neposredno izvršati.Mora iti skozi assembler: orodje, ki te mnemonike in simbole prevede v njihov ekvivalent v binarnem strojnem jeziku.
Programer, ki uporablja asemblerje, mora imeti zelo dobro razumevanje arhitekture procesorja: razpoložljivi registri, nabor ukazov, načini naslavljanja in strojne funkcijeZaradi te tesne povezave s fizično zasnovo je koda hitra in natančna, vendar ne zelo prenosljiva.
Poleg tega se število razpoložljivih ukazov razlikuje glede na vrsto arhitekture. Arhitekture CISC (Complex Instruction Set Computer) običajno vključuje precej bogate nabore ukazov, medtem ko arhitekture RISC (računalnik z zmanjšanim naborom ukazov) se odloči za manjši in bolj homogen repertoardajanje prednosti preprostim in hitrim navodilom.
Jeziki na srednji ravni
Med nizkonivojskimi in visokonivojskimi jeziki so jeziki srednje ravniTi programski jeziki izkoriščajo oba svetova: zagotavljajo priročne nadzorne strukture in funkcije, hkrati pa ponujajo dokaj neposreden dostop do pomnilnika in strojne opreme.
V to skupino spadajo jeziki, kot so C in Basic, na nekoliko višji stopnji, a še vedno blizu Metalu, pa C++, Rust, Fortran, Cobol, Lisp ali GoObičajno so proceduralno usmerjeni (ali objektno in proceduralno usmerjeni, odvisno od primera), pri čemer kodo organizirajo v ponovno uporabne funkcije ali bloke.
Uporabljajo se za razvoj vsega od operacijski sistemi in gonilniki za uporabniške aplikacije kot so preglednice, upravitelji podatkovnih baz ali znanstvenih orodij. V mnogih primerih se za maksimiranje zmogljivosti zanašajo na knjižnice, napisane na nizki ravni ali neposredno v zbirniku.
V tem kontekstu izstopa C, saj omogoča obdelavo kazalcev in podatkovnih struktur. izjemno blizu temu, kako je pomnilnik organiziran v dejanskem računalnikuZaradi tega je lažje pisati "sistemsko" kodo, ne da bi se pri tem popolnoma odpovedali sintaksi, ki je nekoliko bolj uporabniku prijazna kot zbirni jezik.
Kam se C umešča med nizkonivojske jezike?
C je običajno formalno razvrščen kot jezik srednje stopnjeVendar pa ga v mnogih praktičnih razpravah uvrščajo med nizkonivojske jezike, ker ponuja zelo natančen nadzor nad pomnilnikom in strojno opremo. Razvil ga je Dennis Ritchie v Bell Labs v sedemdesetih letih prejšnjega stoletja z jasnim ciljem: zgraditi učinkovite operacijske sisteme in sistemsko programsko opremo.
Ena od velikih prednosti jezika C je, da združuje učinkovitost in prenosljivostLahko napišete kodo, ki je precej blizu dejanskemu delovanju stroja, in jo hkrati prevedete za različne arhitekture z relativno majhnimi spremembami, če le spoštujete posebnosti vsakega sistema.
Na področju "nizkonivojskega C" delamo s koncepti, kot so kazalci, ročno upravljanje pomnilnika, strukture, ki odražajo strojne registre ali medpomnilnikein minimalna uporaba visokonivojskih knjižnic. Cilj je ohraniti nadzor nad zmogljivostjo in porabo virov.
Zaradi tega pristopa je C glavni jezik za naloge, kot so razvoj jeder operacijskih sistemov, gonilnikov naprav, vdelane programske opreme in vgrajenih aplikacij (vgrajeni), kjer je pomemben vsak cikel procesorja in vsak bajt RAM-a.
Čeprav je C v smislu abstrakcije boljši od zbirnega jezika, so sodobni prevajalniki sposobni generirati visoko optimizirana strojna koda iz dobro napisane kode Cv mnogih situacijah dosega zmogljivost, praktično enakovredno ročno napisanemu asemblerskemu programu.
/* Primer "nizkonovnega" jezika C z vrstičnimi komentarji. - Brez stdio: za neposreden izpis na stdout uporabljamo write(). - Manipulacija pomnilnika s kazalci in bitnimi operacijami. - Uporaba volatile za simulacijo dostopa do registrov strojne opreme. */ #include // write() #include // celoštevilski tipi fiksne velikosti #include // size_t // Simulira pomnilniško preslikan strojni "register" (dostop ni optimiziran). static volatile uint8_t REG_CONTROL = 0x00; // volatile: vedno dejansko branje/pisanje // Bitna maska za REG_CONTROL #define CTRL_ENABLE (1u << 0) // bit 0: omogoči #define CTRL_RESET (1u << 1) // bit 1: ponastavitev#define CTRL_IRQ_MASK (1u << 2) // bit 2: maska prekinitve // Preprosta funkcija kopiranja pomnilnika (podobna memcpy), brez optimizacij. static void *mem_copy(void *dst, const void *src, size_t n) { // Pretvori kazalce v bajte za kopiranje oktet za oktetom. uint8_t *d = (uint8_t *)dst; // kazalec cilja kot bajti const uint8_t *s = (const uint8_t *)src; // kazalec izvora kot bajti // Preprosta zanka kopiranja. for (size_t i = 0; i < n; i++) { // iterira skozi vsak bajt d[i] = s[i]; // kopira bajt } return dst; // vrne kazalec cilja } // Funkcija pisanja brez stdio: uporabite write() neposredno. static void putstr(const char *s) { // Ročno izračuna dolžino (do '\0'). size_t len = 0; // začetna dolžina while (s[len] != '\0') { // preide do terminatorja len++; // poveča števec } // Zapiše len bajtov v stdout (fd = 1). (void)write(1, s, len); // write vrne zapisane bajte; tukaj prezri rezultat } // Pretvori bajt v dva šestnajstiška znaka (npr. 0xAF -> "AF") brez dinamične dodelitve. static void byte_to_hex(uint8_t b, char out[2]) { // Lokalna tabela za šestnajstiške števke. static const char HEX[16] = "0123456789ABCDEF"; // šestnajstiške številke out[0] = HEX[(b >> 4) & 0x0F]; // visok nibble out[1] = HEX[b & 0x0F]; // nizek nibble } int main(void) { // Izvorni medpomnilnik z binarnimi podatki. uint8_t src[8] = {0x10, 0x3F, 0xA5, 0x00, 0x7C, 0xFF, 0x01, 0xB2}; // začetni bajti // Ciljni medpomnilnik, kamor bomo kopirali. uint8_t dst[8] = {0}; // inicializirano na nič // Pomnilnik kopiramo ročno. mem_copy(dst, src, sizeof(src)); // kopira 8 bajtov iz src v dst // Aktiviramo kontrolne bite "registra", ki simulirajo konfiguracijo naprave. REG_CONTROL |= CTRL_ENABLE; // Omogoči napravo (nastavi bit 0) REG_CONTROL &= (uint8_t)~CTRL_RESET; // Zagotavlja, da je ponastavitev onemogočena (počisti bit 1) REG_CONTROL |= CTRL_IRQ_MASK; // Maskira prekinitve (nastavi bit 2) // Zapišemo glavo. putstr("Stanje REG_CONTROL in vsebina medpomnilnika:\n"); // začetno sporočilo // Prikaži REG_CONTROL v šestnajstiški obliki. char hx[2]; // dva znaka za en bajt v šestnajstiškem zapisu byte_to_hex(REG_CONTROL, hx); // pretvori vrednost registra putstr("REG_CONTROL = 0x"); // predpona (void)write(1, hx, 2); // zapiše dve šestnajstiški števki putstr("\n"); // nova vrstica // Prečkamo ciljni medpomnilnik in prikažemo indekse in vrednosti v šestnajstiškem zapisu. for (size_t i = 0; i < sizeof(dst); i++) { // iteracija za vsak bajt putstr("dst["); // začetek indeksa oznake // Indeks pretvorimo v števko (zaradi enostavnosti predpostavimo <10). char idx = (char)('0' + (int)i); // indeksni znak (void)write(1, &idx, 1); // zapiše indeks putstr("] = 0x"); // ločilo in šestnajstiška predpona byte_to_hex(dst[i], hx); // pretvori bajt v šestnajstiško vrednost (void)write(1, hx, 2); // zapiše šestnajstiško vrednost putstr("\n"); // nova vrstica za vsakim vnosom } // Demonstracija kazalca in aritmetike: povečaj tretji bajt z uporabo kazalca. uint8_t *p = &dst[2]; // kazalec na tretji element *p += 1; // poveča njegovo vrednost za 1 // Prikaže uporabljeno spremembo. putstr("Spremenjeno dst[2] + 1 -> 0x"); // sporočilo byte_to_hex(dst[2], hx); // po spremembi preračuna šestnajstiško vrednost (void)write(1, hx, 2); // izpiše novo vrednost putstr("\n"); // konec vrstice return 0; // zaključi brez napak }
Ključne značilnosti nizkonivojskih jezikov
Nizkonajnostni jeziki (in v veliki meri C, ki se uporablja s sistemsko miselnostjo) Imajo številne skupne značilnosti ki narekujejo, kako se z njimi programira. To so močne prednosti, vendar imajo tudi določene pomanjkljivosti, ki jih je treba razumeti.
Največja združljivost strojne opreme
Ti jeziki omogočajo doseganje zelo tesna povezava med tem, kar programer napiše, in tem, kako se uporabljajo viri strojaVsak ukaz se prevede v zelo specifične operacije na registrih, pomnilniku ali perifernih napravah.
Ko programirate na nizki ravni, se zanašate neposredno na nabor ukazov in arhitekturne načrte: Veste, v kateri register gre vsak podatek, kako je pomnilnik naslovljen in kakšna je latenca vsake operacije.To je idealno za kar najboljši izkoristek določenega procesorja, na primer v vgrajenih sistemih ali komponentah v realnem času.
Hitrost in zmogljivost
Z odpravo vmesnih plasti je mogoče izvajanje programov, napisanih v strojnem jeziku, zbirnem jeziku ali jeziku C, ki so zelo blizu strojne opreme, poenostaviti. izjemno hitroNi tolmačev oz. navidezni stroji delujejo kot posredniki, popoln nadzor nad viri pa omogoča zelo specifične optimizacije.
To pomeni, da se te vrste jezikov uporabljajo v programska oprema, kjer je zmogljivost ključnega pomena: nizkonivojski mehanizmi baz podatkov, deli operacijskih sistemov, intenzivna obdelava podatkov, kompresorji, komunikacijski sistemi itd.
Zelo omejena abstrakcija in prenosljivost
Manj prijetna stran je, da Abstrakcija je minimalna, prav tako pa tudi prenosljivost.Koda, zasnovana za določeno arhitekturo, pogosto zahteva spremembe, če jo želimo izvajati na drugi platformi z drugačnim naborom ukazov ali modelom pomnilnika.
Poleg tega odsotnost zaščitnih plasti pomeni, da Vsaka napaka lahko povzroči neveljaven dostop do pomnilnika, zaklepanje ali tihe napake.Programer je odgovoren za vzdrževanje reda in varnosti, z minimalno pomočjo jezika.
Kompleksnost branja in pisanja
Čeprav sta zbirni jezik in zelo nizkonivojski C bolj berljiva kot zaporedje bitov, je za večino razvijalcev Težko jih je brati, pisati in vzdrževati v primerjavi s sodobnim jezikom visoke ravni.
Za program nizke ravni je običajno, da zahteva zelo stroga organizacija in skrbna dokumentacijaravno zato, ker deluje s tako finimi strojnimi podrobnostmi, da ima lahko vsaka majhna sprememba kompleksne stranske učinke.
Neposredna interakcija s strojno opremo
V nizkonivojskih jezikih interakcija s strojno opremo običajno poteka brez posrednikov: Manipulirajo se registri naprav, vhodno/izhodna vrata in pomnilniška območja, preslikana na periferne naprave.To nam omogoča, da naredimo stvari, ki bi bile nemogoče ali zelo zapletene pri uporabi čistih visokonivojskih jezikov.
V mnogih primerih se jeziki visoke ravni zanašajo na moduli, napisani v jeziku C ali assembler dostopati do občutljivih ali kritičnih funkcij, prav zaradi te zmožnosti neposrednega komuniciranja s strojno opremo.
Pogoste uporabe nizkonivojskih jezikov
Nizkonajavni jeziki igrajo temeljno vlogo v programskih plasteh, ki morda niso vidne na prvi pogled, vendar so tiste, ki ohranjajo delovanje vsega. Večina uporabnikov se te kode nikoli ne bo dotaknila, vendar Odvisni so od tega, da bo njihov sistem stabilen in hiter..
Klasičen primer je tisti, OSVelik del njegovih jeder in kritičnih komponent je bil v preteklosti napisan v jeziku C, včasih z odlomki zbirnega jezika za zelo specifične odseke, kot je škorenj ali upravljanje prekinitev.
Druga tipična uporaba je razvoj gonilniki napravTi moduli služijo kot most med operacijskim sistemom in dejansko strojno opremo (omrežne kartice, grafične kartice, naprave). shranjevanjeitd.) in zahtevajo neposreden dostop do določenih registrov in pomnilniških naslovov naprave.
Svet vgrajena programska oprema (mikrokrmilniki, industrijski krmilni sistemi, avtomobilska elektronika, naprave Internet stvariPrav tako se močno zanaša na nizkonivojski C in včasih na zbirni jezik, ker so viri tako omejeni, da šteje vsak bajt.
Končno obstaja veliko komponent visoko zmogljive aplikacije (mehanizmi baz podatkov, knjižnice za šifriranje, stiskanje, grafika itd.), ki so implementirani v jeziku C s filozofijo, ki je zelo blizu nizki ravni, da bi dosegli zmogljivost, ki bi jo drugi jeziki sami po sebi težko dosegli.
Vse te nizkonivojske plasti skupaj delujejo kot "okostje", na katerem so zgrajeni uporabniku prijaznejši jeziki in ogrodja višje ravni, kar daje predstavo o strateški pomen obvladovanja te vrste programiranja če želite delati blizu jedra sistemov.
Po pregledu vseh teh konceptov postane bolj jasno, zakaj se toliko govori o Primeri nizkonivojskega jezika C pri poučevanju osnov sistemov in arhitektureC služi kot praktični most med teorijo strojne opreme (biti, registri, ukazi) in konstrukcijo resnične programske opreme, ki deluje z največjo hitrostjo in z natančnim nadzorom nad strojem.
Strasten pisec o svetu bajtov in tehnologije nasploh. Rad delim svoje znanje s pisanjem in to je tisto, kar bom počel v tem blogu, saj vam bom pokazal vse najbolj zanimive stvari o pripomočkih, programski opremi, strojni opremi, tehnoloških trendih in še več. Moj cilj je, da vam pomagam krmariti po digitalnem svetu na preprost in zabaven način.
