The DNS protocol specification is pretty simple, and is available for anyone to read here. This post is about picking up the bare-minimum parts to build a simple DNS query, and then reading the IP in the server's response message. Lets try to lookup IP address for "www.wikipedia.org".

A DNS query consists of a header, and a "Question" section.

Query Header

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      ID                       |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |QR|   Opcode  |AA|TC|RD|RA|   Z    |   RCODE   |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    QDCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ANCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    NSCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                    ARCOUNT                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The only fields that we'll "set" are message ID (set to any 16-bit value), which is copied back in server response; RD (set to 1), which tells the DNS servers to perform lookup recursively if needed; and QDCOUNT (set to 1), which says that we only have one domain to lookup. Everything else will be set to 0.

Question Section

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                     QNAME                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QTYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     QCLASS                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

The domain name is set in QNAME. Instead of putting the domain name straight away, you have to split the domain name in "labels". Here, our labels will be "www", then "wikipedia", and then "org". QNAME requires length to be prefixed before each label. In our case, QNAME will be:
3, w, w, w, 9, w, i, k, i, p, e, d, i, a, 3, o, r, g, 0

Note that the QNAME must end with a 0.

QTYPE and QCLASS will both be set to 1.

Server Response

                                    1  1  1  1  1  1
      0  1  2  3  4  5  6  7  8  9  0  1  2  3  4  5
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                                               |
    /                                               /
    /                      NAME                     /
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TYPE                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                     CLASS                     |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                      TTL                      |
    |                                               |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+
    |                   RDLENGTH                    |
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--|
    /                     RDATA                     /
    /                                               /
    +--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+--+

If you get a server response at all, the IP address will be in the last 4 bytes of RDATA. Everything else can be ignored for our simple case.

Code

For the curious, here is a small C snippet that sends the query and prints the IP address.

#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
#include <unistd.h>

int main(void)
{
	int i;
	uint8_t rsp[512];
	uint8_t qry[] = {
		0xde,0xad,		//ID
		0x01,0x00,		//RD set
		0x00,0x01,		//QDCOUNT = 1
		0x00,0x00,		//ANCOUNT = 0
		0x00,0x00,		//NSCOUNT = 0
		0x00,0x00,		//ARCOUNT = 0
		3,
		'w','w','w',
		9,
		'w','i','k','i','p','e','d','i','a',
		3,
		'o','r','g',
		0,
		0x00,0x01,	//QTYPE
		0x00,0x01,	//QCLASS
	};

	struct sockaddr_in sv;
	socklen_t len;
	int s = socket(AF_INET,SOCK_DGRAM,IPPROTO_UDP);

	sv.sin_family = AF_INET;
	sv.sin_port = htons(53);
	sv.sin_addr.s_addr = inet_addr("8.8.8.8");

	sendto(s,qry,sizeof(qry),0,(struct sockaddr*)&sv,sizeof(sv));
	i = recvfrom(s,rsp,512,0,(struct sockaddr*)&sv,&len);

	printf("IP address is %d.%d.%d.%d\n",rsp[i-4],rsp[i-3],rsp[i-2],rsp[i-1]);

	close(s);
	return 0;
}