Le message suivant est tiré d'un référentiel que j'ai créé pour aider à apprendre (et enseigner) les opérations au niveau des bits. Vous pouvez trouver ce dépôt ici, et je vous suggère de le consulter : vous y trouverez des exemples de code et des solutions.
Le but de ce référentiel est de vous familiariser avec les opérations au niveau du bit, en expliquant ce qu'elles sont, comment elles fonctionnent et à quoi elles peuvent être utilisées.
En C (et dans la plupart des langages de haut niveau), nos variables ont destypes. Ces types sont révélateurs de plusieurs choses. Bien sûr, une variable de type int stockera une valeur entière, mais la clé pour comprendre ces opérations au niveau du bit est de savoir que sous le capot, tous les types sont stockés en mémoire (n'importe où, pile, tas, n'importe où) sous forme binaire. Voici un exemple de ce qui se passe lorsque vous stockez une simple valeur entière sur la pile dans C:
int main(int argc, char** argv) { int x = 2; return 0; }
Après la compilation en assembly, le code pourrait ressembler à ceci (j'utilise ici l'assembly ARM et j'ai annoté le code à l'aide de commentaires) :
.section .text .global main main: ; Set up the stack for the function stp x29, x30 [sp, -16]! ; Save previous frame pointer & link register mov x29, sp ; Setup a new frame pointer ; Initialize x with 2 ; IN C: int x = 2; mov w0, 2 ; Move 2 into the w0 register str w0, [sp, 16] ; Store the contents of w0 (2) at a 16-byte offset from the stack pointer ; Essentially, the above line stores 2 on the stack. mov w0, 0 ; Move 0 into w0, prepare for return ; Clear stack ldp x29, x30, [sp], 32 ; Restore frame pointer and link register ret ; Return
Notez que la plupart des compilateurs ne stockeraientpasréellement une variable comme celle que j'ai montrée sur la pile, car elle n'est pas utilisée. Cependant, s'il est utilisé plusieurs fois, il sera stocké sur la pile, comme ci-dessus.
Si nous regardions l'emplacement où notre variable a été stockée sur la pile (alors qu'elle est là, bien sûr), nous verrions quelque chose comme :
Adresse mémoire | Valeur stockée (Hex) | Valeur stockée (binaire) |
---|---|---|
0x1000 | 0x02 | 00000010 |
0x1001 | 0x00 | 00000000 |
0x1002 | 0x00 | 00000000 |
0x1003 | 0x00 | 00000000 |
Cela suppose que votre système est petit-boutiste. Je n'entrerai pas dans les détails de l'endianisme ici, mais vous pouvez en savoir plus ici.
La chose clé que j'aimerais que vous remarquiez à propos du tableau ci-dessus est que même si notre entier ne fait que 2 bits, il occupe 4 octets (32 bits) de mémoire. Maintenant, ne paniquez pas, c'est normal et attendu. L'une des nombreuses choses que font C et votre compilateur est de définir des normes pour les types que vous invoquez. Ainsi, lorsque je crée une variable int, le compilateur sait allouer 4 octets (encore une fois, 32 bits) de mémoire. Nous pouvons même tester cela en utilisant l'opérateur sizeof() en C.
L'opérateur sizeof()
sizeof() n'est pas une véritable fonction C. Au lieu de cela, au moment de la compilation, le compilateur remplace l'expression par la taille du type de données donné. Vous pouvez même l'utiliser avec vos propres types, comme les typedefs et/ou les structures :
#includetypedef struct { char name[64]; int age; } Person; int main(int argc, char** argv) { printf("A Person is %lu bytes long.\n", sizeof(Person)); return 0; } Copier après la connexion
Une autre chose que vous vous demandez peut-être est de savoir comment les nombres négatifs sont stockés. Excellente question. Les numéros peuvent êtresignésounon signés, mais par défaut, ils sont signés. Si un entier est signé, il sacrifie son bit le plus significatif pour devenir le « bit de signe ». Si le bit de signe est 1, le nombre est négatif ; sinon c'est positif. Un lecteur avisé se rendra peut-être compte que le changement qui se produit ici se situe dans la gamme des nombres possibles. Considérez les nombres de 8 bits. Il y a 256 nombres possibles à représenter (donnés par 2^8). Avec un entier non signé de 8 bits, les valeurs 0 à 255 peuvent être représentées ; avec un entier 8 bits signé, -128-127 peut être représenté.
Pour obtenir l'inverse d'un nombre binaire, utilisez le complément de deux. Trouvons -5 en binaire.
Type | Taille (octets) | Taille (bits) |
---|---|---|
int | ||
int64_t | ||
int8_t | ||
char | ||
bool (vous devrez #include
|
||
long int | ||
int court | ||
long long int | ||
double | ||
double |
Here's an example of what your simple program might look like (you can also check it out at Chapter 1/SizeOfOperatorTest.c).
#includeint main(int argc, char** argv) { printf("The size of an int is %lu bytes, or %lu bits.\n", sizeof(int), sizeof(int) * 8); return 0; }
Go ahead and compile it using gcc and check out its output:
cd Chapter\ 1 gcc -o sizeof SizeOfOperatorTest.c ./sizeof
Type | Size (bytes) | Size (bits) |
---|---|---|
int | 4 | 32 |
int64_t | 8 | 64 |
int8_t | 1 | 8 |
char | 1 | 8 |
bool (you'll need to #include
|
1 | 8 |
long int | 4 | 32 |
short int | 2 | 16 |
long long int | 8 | 64 |
double | 4 | 32 |
double | 8 | 64 |
TakeThisHome
The main point I'd like you to keep in mind is thatwith control of every bit, we can optimize our memory usage. Though that has little effect on modern systems, in the case of embedded computing, every byte counts.By manually reading and writing bits as opposed to typical variable values, we can harness more functionality from less storage.
Now that we've covered data types and how data is stored, we're ready to introduce the idea of bitwise operations. Put simply, abitwise operationis an operation done on each bit of a piece of data. Let's take a look at each bitwise operator. Then, we'll implement them in C.
Written x & y in C. This operator takes the corresponding bits of each number and performs an and operation. An and operation returns 1 (true)if and only if both bits are 1. This means that two bits that are both 0 donotreturn 1—they return 0. The result is the number made up of the binary string of results from each and. It's easiest to see this in a truth table.
Consider the operation int result = 3 & 5. First, convert 3 and 5 to binary.
Now, we have int result = 011 & 101. Consider the following truth table:
Bit A | Bit B | AND |
---|---|---|
0 | 1 | 0 |
1 | 0 | 0 |
1 | 1 | 1 |
The result of the and operation is 001, which when converted to decimal is 1.
Written x | y in C. This operator takes the corresponding bits of each number and performs an or operation. An or operation returns 1if either bit is 1. As with other bitwise operators, the result is the number made up of the binary string of results from each or.
Consider the operation int result = 3 | 5. First, convert 3 and 5 to binary.
Now, we have int result = 011 | 101. Consider the following truth table:
Bit A | Bit B | OR |
---|---|---|
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 1 |
The result of the or operation is 111, which when converted to decimal is 7.
Written x ^ y in C. This operator takes the corresponding bits of each number and performs an xor (exclusive or) operation. An xor operation returns 1if and only if one of the two bits is 1. As with other bitwise operators, the result is the number made up of the binary string of results from each or.
Consider the operation int result = 3 ^ 5. First, convert 3 and 5 to binary.
Now, we have int result = 011 ^ 101. Consider the following truth table:
Bit A | Bit B | XOR |
---|---|---|
0 | 1 | 1 |
1 | 0 | 1 |
1 | 1 | 0 |
Das Ergebnis der xor-Operation ist 110, was bei der Umrechnung in eine Dezimalzahl 6,
ergibtGeschrieben x << Betrag Im Gegensatz zu den oben genannten Operatoren arbeitet dieser Operator nur mit einer Zahl. Jedes Bit in der angegebenen Zahl wird um den angegebenen Betrag nach links verschoben. Alle Bits, die das Ende der Zahl erreichen, werden abgeschnitten (und auf der rechten Seite erscheinen Nullen). Lassen Sie uns ein Beispiel durchgehen.
Consider int result = 5 << 2. Wie Sie wissen, ist 5 binär 101. Lassen Sie uns jede Iteration der Schicht durchgehen.
Anfänglich
1 | 0 | 1 |
---|
Nach einer Schicht
0 | 1 | 0 |
---|
Ergebnis
1 | 0 | 0 |
---|
Das binäre Ergebnis ist 100, also 4 in Dezimalzahl.
Geschrieben x >> Betrag Dieser Operator ähnelt der Linksverschiebung, funktioniert jedoch in die entgegengesetzte Richtung. Jedes Bit in der angegebenen Zahl wird um den angegebenen Betrag nach rechts verschoben. Alle Bits, die das Ende der Zahl erreichen, werden abgeschnitten (und auf der linken Seite erscheinen Nullen). Lassen Sie uns ein Beispiel durchgehen.
Consider int result = 3 >> 2. Wie Sie wissen, ist 3 binär 011. Lassen Sie uns jede Iteration der Schicht durchgehen.
Anfänglich
0 | 1 | 1 |
---|
Nach einer Schicht
0 | 0 | 1 |
---|
Ergebnis
0 | 0 | 0 |
---|
Das binäre Ergebnis ist 000, was 0 in Dezimalzahl ist.
Geschrieben ~x. Der Not-Operator invertiert alle Bits der angegebenen Zahl. Zur Erläuterung verwenden wir noch einmal eine Wahrheitstabelle.
Betrachten Sie das int-Ergebnis = ~5. Wie Sie wissen, ist 5 binär 101.
Bit A | ~ Bit A |
---|---|
1 | 0 |
0 | 1 |
1 | 0 |
Daher ist das Ergebnis der Nicht-Operation 010, was binär 2 ist.
Einschränkungen für die Links- und Rechtsverschiebung
Für diesen Schichtbetrieb gelten einige nennenswerte Einschränkungen. Zunächst einmal können Sie Bits nicht um eine negative Anzahl verschieben – das macht einfach keinen Sinn! Außerdem gilt das Verschieben um mehr als die Anzahl der Ihrer Variablen zugewiesenen Bits alsundefiniert. Siekönnenes tun, aber es ist nicht garantiert, dass die Ausgabe für einen bestimmten Wert konstant ist. Obwohl es sich hierbei nicht um eine Einschränkung per se handelt, führt eine Verschiebung um 0 einfach nicht zu einer Verschiebung.
(5 & 2) ^ (4 | 3)
Schließen Sie jeden Vorgang ab. Betrachten Sie alle Werte als vorzeichenlos und solange der längste Wert im Problem erforderlich ist (d. h. wenn Sie den größten Wert von 8 haben, müssen Sie sich mit 4 Bits befassen). Konvertieren Sie es nach Abschluss in eine Dezimalzahl.
~6
9 << 4 (hier wird davon ausgegangen, dass der Wert eine Länge von 32 hat, sodass Platz für eine Linksverschiebung vorhanden ist).
~(7 & 8)
(2 | 6) >> 1
8 >> (~2)
~((3 >> 2) ^ ~7) & (~(7 >> 4) | 2)
Bit A | Bit B | UND |
---|---|---|
1 | 0 | 0 |
0 | 0 | 0 |
0 | 1 | 0 |
0 | 0 | 0 |
⇒ 0000, was 0 in Dezimalzahl ist.
Bit A | Bit B | ODER |
---|---|---|
1 | 0 | 1 |
1 | 1 | 1 |
0 | 1 | 1 |
⇒ 111, also 7 in Dezimalzahl.
Bit A | Bit B | XOR |
---|---|---|
1 | 1 | 0 |
1 | 0 | 1 |
1 | 1 | 0 |
⇒ 010, was 2 in Dezimalzahl ist.
Bit A | Bit B | A und B | Bit C | Bit D | C UND D | (A UND B) UND (C UND D) |
---|---|---|---|---|---|---|
1 | 0 | 0 | 1 | 0 | 0 | 0 |
0 | 1 | 0 | 0 | 1 | 0 | 0 |
1 | 0 | 0 | 0 | 1 | 0 | 0 |
⇒ 000, was 0 in Dezimalzahl ist.
Bit A | Bit B | A ODER B | Bit C | Bit D | C UND D | (A ODER B) UND (C UND D) |
---|---|---|---|---|---|---|
1 | 0 | 1 | 1 | 0 | 0 | 0 |
0 | 1 | 1 | 0 | 1 | 0 | 0 |
1 | 0 | 1 | 0 | 1 | 0 | 0 |
⇒ 000, was 0 in Dezimalzahl ist.
Bit A | Bit B | A und B | Bit C | Bit D | C ODER D | (A UND B) XOR (C ODER D) |
---|---|---|---|---|---|---|
1 | 0 | 0 | 1 | 0 | 1 | 1 |
0 | 1 | 0 | 0 | 1 | 1 | 1 |
1 | 0 | 0 | 0 | 1 | 1 | 1 |
⇒ 111, which is 7 in decimal.
~6 → ~110 ⇒ 011, which is 3 in decimal.
9 << 4 → 001001 << 4 ⇒ 100100, which is 36 in decimal.
~(7 & 8) → ~(0111 & 1000) → ~(0000) ⇒ 1111, which is 15 in decimal.
(2 | 6) >> 1 → (010 | 110) >> 1 → 110 >> 1 ⇒ 011, which is 3 in decimal.
8 >> (~2) → 1000 >> ~(10) → 1000 >> (01) → 1000 >> 1 ⇒ 0100, which is 4 in decimal.
~((3 >> 2) ^ ~7) & (~(7 >> 4) | 2)
To solve this, I suggest splitting into halves:
~((3 >> 2) ^ ~7) and (~(7 >> 4) | 2)
~((3 >> 2) ^ ~7) → ~((011 >> 2) ^ ~(111)) → ~((000) ^ ~(111)) → ~(000 ^ 000) → 111
(~(7 >> 4) | 2) → (~(111 >> 4) | 010) → (~(000) | 010) → (111 | 010) → 111
Hence, 111 & 111 ⇒ 111, which is 7 in decimal.
This chapter is all about writing C code that utilizes bitwise operators. Before we get to doing bitwise operations, we should begin by writing a function that can write the binary equivalent of a given variable.
To do this, we use a mask. We initialize it to contain a 1 in the most significant (leftmost in little-endian systems) bit followed by zeros. With each iteration of a loop, we right shift the mask by 1, moving the 1 all the way "across" the mask. When we use the & operator on the pointer and the mask, any non-zero value means that a 1 occurred somewhere in the result. Since there's only one 1 in the mask, we know exactly where this occurred. Since the loop moves from left to right, we can just append the corresponding bit's character to the string. The string is one character longer than the size because it needs to contain the null character (\0). This is how printf knows to stop printing, and omitting it can lead to numerous errors and/or unexpected behaviors (like the printing of other data from in memory).
void printBinary(unsigned int decimal) { // To determine size (in bits), we multiply the maximum size of an unsigned int by 8 (to convert from bytes to bits). int size = sizeof(decimal) * 8; // This counts the leading zeros of the number, then subtracts them from the size. // Hence, we start at the first bit set to 1 (unless decimal == 0) size -= __builtin_clz(decimal); if(size == 0) size = 1; // Handle zero case, we need one space to display "0." int curr = 0; char binary[size + 1]; for(unsigned int mask = 1 << (size - 1); mask != 0; mask >>= 1) { if((decimal & mask) != 0) { binary[curr] = '1'; } else { binary[curr] = '0'; } curr++; } binary[curr] = '\0'; printf("%s", binary); }
Bitwise Assignment Operators
All bitwise operators, except for not (~), can be used in the assignment fashion. This means you can add an equals sign right next to one of the bitwise operator. For example, in
int a = 2; a &= 7;Copier après la connexiona is both the first operand and the destination. In other words, the value of a & 7 is stored in a. This works for all bitwise operators aside from the not (~) operator.
Now, I'd like to provide a few case study like prompts for you to try. Sample responses are available.
One use case of bitwise operations is in the Unix permission system. You've probably run the command
chmod 777 some-file
But what do each of the numbers mean? And why 7? The reality is, binary is at play here, and 7 should tip you off. Recall that 7 in binary is 111. The number being passed here is acting as three flags or booleans glued together.
The three digits specified are for three classes of users:
As I mentioned above, each digit is a conglomeration of three flags, each representing a permission. The flag (binary bit) in the fours place represents read permission, the twos place is for write permission, and the ones is for execute. So,
chmod 777 some-file
is doing this under the hood:
Group | Read | Write | Execute | Decimal |
---|---|---|---|---|
Owner | 1 | 1 | 1 | 7 |
Owner's Group | 1 | 1 | 1 | 7 |
Everyone Else | 1 | 1 | 1 | 7 |
Mit anderen Worten, alle Berechtigungen werden allen erteilt.
Entwerfen Sie einen einfachen Berechtigungsprüfer, der einen vollständigen Dateiberechtigungswert (eine dreistellige Zahl) aufnimmt und nach einem bestimmten Satz spezifischer Berechtigungen sucht (z. B. Schreiben durch den Besitzer, Ausführen durch alle usw.). Ein Beispiel finden Sie im Ordner Kapitel 3.
Nachdem Sie eine vollständige Zahl eingegeben haben, müssen Sie diese in ein int (von einem char*) umwandeln. Verwenden Sie dann modulare Arithmetik, um jeden Berechtigungssatz aufzuschlüsseln. Denken Sie daran, dass die erste Ziffer die Berechtigungen des Eigentümers darstellt, die zweite für die Benutzergruppe des Eigentümers und die dritte für alle.
Um zu überprüfen, ob eine bestimmte Berechtigung in einem Berechtigungssatz vorkommt, bitweise und die gegebene Berechtigung mit dem Satz.
Wenn Sie jemals einen Router konfiguriert haben, ist Ihnen möglicherweise eine Option zum Konfigurieren der „Subnetzmaske“ aufgefallen. Normalerweise ist dies auf 255.255.255.0 eingestellt, aber warum? Zunächst müssen wir bedenken, dass jedes Byte einer IPv4-Adresse durch ein „.“ getrennt ist. Wenn Sie es mit dem Netzwerktyp zu tun haben, mit dem Sie am besten vertraut sind (ein Netzwerk der Klasse C), sind 3 Bytes für das Netzwerk reserviert und das letzte Byte ist für den Host.
Da es sich bei der Subnetzmaske um eine Maske handelt, verstehen Sie vielleicht schon, welchen Zweck sie hat. Für jedes Bit, das Sie vom Host-Byte „ausleihen“, werden zwei Subnetze erstellt.
Netzwerkadresse
Bei derNetzwerkadressesind alleHost-Bits auf 0 gesetzt. Dies bedeutet, dass jedes Bit zum Erstellen übergeben wird
Ein Subnetz könnte immer noch auf 1 gesetzt werden.Mehr lesen!
Erfahren Sie mehr über Subnetze auf dieser Website.
Schreiben Sie in C ein Programm, das eine IPv4-Adresse und eine Subnetzmaske aufnimmt und die Netzwerkadresse findet und ausgibt, in der sich die IPv4-Adresse befindet. Ein Beispiel finden Sie im Ordner Kapitel 3.
Sie müssen jedes Byte der Adresse und Maske als numerischen Wert speichern. Um die Netzwerkadresse zu finden, überlegen Sie, welche (bitweise) Operation zwischen Maske und Adresse den beabsichtigten Effekt erzeugt.
Ich hoffe, dieser Erklärer war hilfreich für Sie! Ich habe es geschrieben, weil ich selbst etwas über bitweise Operationen lernen wollte. Ich habe es überprüft, aber einige Dinge könnten falsch sein. Korrigieren Sie mich also gerne über eine Pull-Anfrage oder fügen Sie sogar einen Kommentar hinzu! Wenn Sie Fragen haben, hinterlassen Sie außerdem einen Kommentar! Ich kann es kaum erwarten, mit Ihnen zu chatten! Abschließend möchte ich sagen, dass ich sehr froh bin, Ihnen diese Ressource zur Verfügung stellen zu können!
Hallo! Ich bin Jackson, Student der Informatik und Französisch am Lafayette College und ein aufstrebender Forscher und Professor für Informatik. Ich interessiere mich derzeit für die Bereiche Bioinformatik und Low-Level-Programmierung/Systeme. Um mehr über mich zu erfahren, schauen Sie sich meine Website an.
Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!