Looking at arrays with SOS on WinDBG is not exactly the most intuitive process in the world.  In order to demonstrate how to do this, I've written a small sample program that I'm going to “debug”.  The code simply creates three arrays:

string[] strs = new string[] { "String 1", "String 2", "String 3" };
int[] ints = new int[] { 21, 22, 23, 24, 25, 26, 27 };
double[] fps = new double[] { 3.14 };

All of the information in this post was done debugging Whidbey, though it should still apply to v1.0 and v1.1 of the runtime. This was also all done on x86. If you're using IA64 or AMD64 then obviously the array format will be slightly different (there will be more padding, and the addresses will be 64 bits instead of 32), but the general format will remain the same, and you should be able to figure out what's going on from looking at memory dumps.

The first step is to get the addresses of these arrays (I'll edit all of my WinDBG outputs to focus on the relevant portions):

0:000> !sos.dso
ESP/REG Object Name
0012ec94 00ac2ad8 System.Double[]
0012eca8 00ac2a88 System.Object[]
0012ecac 00ac2aa4 System.Int32[]

Arrays of Native Types

In order to look at arrays of native types (here, int and double), just doing a standard !do doesn't provide much information. Working with my integer array:

0:000> !do 00ac2aa4
Name: System.Int32[]
MethodTable: 5b9ca974
EEClass: 5ba4d4ec
Size: 40(0x28) bytes
Array: Rank 1, Type Int32
Element Type: System.Int32
Fields:
None

Instead of using SOS to look at this memory, I'll instead just dump the memory contents at that address:

0:000> dd 00ac2aa4
00ac2aa4  5b9ca974 00000007 00000015 00000016
00ac2ab4  00000017 00000018 00000019 0000001a
00ac2ac4  0000001b 00010000 5b9938b0 00000000
00ac2ad4  00010000 5b9c9b18 00000001 51eb851f
00ac2ae4  40091eb8 00010000 5b9b2be0 00000006
00ac2af4  00000000 00000080 00010000 5b9c6614
00ac2b04  00000001 5b9938b0 00ac2aec 00010000
00ac2b14  5b9b2c8c 00000080 00010000 5b9b413c

I've highlighted relevant portions. The first word, in green, is the method table for the type of array. If you do a !dumpmt on this address, you'll see that this is an array of integers:

0:000> !dumpmt 5b9ca974
EEClass: 5ba4d4ec
Module: 5b918000
Name: System.Int32[]
mdToken: 02000000 (C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x4
Number of IFaces in IFaceMap: 4
Slots in VTable: 25

The second word, in blue, is the number of elements in the array. In this example, there are 7 elements. The next seven words, shown here in maroon, are the actual integers stored in the array. You can see they're hex values 0x15 through 0x1b, which are 21-27 in decimal ... the values in the array

The floating point array works much the same. From the first !dso, remember that the floating point array starts at address 00ac2ad8. Running a !do on this just says that this is an array of doubles:

0:000> !do 00ac2ad8
Name: System.Double[]
MethodTable: 5b9c9b18
EEClass: 5ba4cc30
Size: 20(0x14) bytes
Array: Rank 1, Type Double
Element Type: System.Double
Fields:
None

To get useful information, once again dump the memory at the address:

0:000> dd 00ac2ad8
00ac2ad8  5b9c9b18 00000001 40091eb8 51eb851f
00ac2ae8  00010000 5b9b2be0 00000006 00000000
00ac2af8  00000080 00010000 5b9c6614 00000001
00ac2b08  5b9938b0 00ac2aec 00010000 5b9b2c8c
00ac2b18  00000080 00010000 5b9b413c 00ac2b80
00ac2b28  00010100 00000000 00010000 5b9b4668
00ac2b38  00000000 00000009 0000000a 00010000
00ac2b48  5b9c6614 0000000a 5b9938b0 00000000

Again, I've highlighted the relevant portions. The first word, in green, is once again the method table for this array. Dumping that shows we have an array of doubles:

0:000> !dumpmt 5b9c9b18
EEClass: 5ba4cc30
Module: 5b918000
Name: System.Double[]
mdToken: 02000000 (C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0xc
ComponentSize: 0x8
Number of IFaces in IFaceMap: 4
Slots in VTable: 25

The next word, in blue, is the number of elements in the array. This shows there's only one element. Since a double is 64 bits, the next two words form the value of this element. However, they are currently arranged in little endian format, and they need to be in big endian. To get this value, simply put the second word before the first. The value of this double is therefore: 0x40091eb851eb851f. Since I haven't memorized the IEEE floating point format, and certainly can't just interpret it by sight, I'll use the WinDBG .formats command to translate this value:

0:000> .formats 40091eb851eb851f Evaluate expression:
  Hex:     40091eb8`51eb851f
  Decimal: 4614253070214989087
  Octal:   0400110753412172702437
  Binary:  01000000 00001001 00011110 10111000 01010001 11101011 10000101 00011111
  Chars:   @...Q...
  Time:    Fri Dec 27 09:23:41.498 16222 (GMT-7)
  Float:   low 1.26444e+011 high 2.1425
  Double:  3.14

You can see that it translates to the expected value of 3.14

Arrays of objects

Arrays of objects work a little differently. Running a !do on one shows the usual information:

0:000> !do 00ac2a88
Name: System.Object[]
MethodTable: 5b9c6614
EEClass: 5ba4b89c
Size: 28(0x1c) bytes
Array: Rank 1, Type CLASS
Element Type: System.Object
Fields:
None

So lets move on to dumping the memory directly, where I'll again highlight the relevant portions:

0:000> dd 00ac2a88
00ac2a88  5b9c6614 00000003 5b99452c 00ac2a1c
00ac2a98  00ac2a40 00ac2a64 00010000 5b9ca974
00ac2aa8  00000007 00000015 00000016 00000017
00ac2ab8  00000018 00000019 0000001a 0000001b
00ac2ac8  00010000 5b9938b0 00000000 00010000
00ac2ad8  5b9c9b18 00000001 51eb851f 40091eb8
00ac2ae8  00010000 5b9b2be0 00000006 00000000
00ac2af8  00000080 00010000 5b9c6614 00000001

The first two words should be familiar, the first one is the method table for the type of array, and the second one is the number of elements in the array. The third word, highlighted in purple, is interesting. It's the address of the method table of the type of object contained in the array. Since running a !dumpmt on the first word only shows us that this is an array of objects, we have to do a !dumpmt on the third word to figure out what kind of objects they are:

0:000> !dumpmt 5b9c6614
EEClass: 5ba4b89c
Module: 5b918000
Name: System.Object[]
mdToken: 02000000 (C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x4
Number of IFaces in IFaceMap: 4
Slots in VTable: 25


0:000> !dumpmt 5b99452c
EEClass: 5b9d7f10
Module: 5b918000
Name: System.String
mdToken: 02000027 (C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
BaseSize: 0x10
ComponentSize: 0x2
Number of IFaces in IFaceMap: 5
Slots in VTable: 190

From the previous method tables, and from looking at the second word, we see we have an array of 3 objects of type System.String. The three words following the type method table address are the addresses of the items stored in the array. You can simply run a !do on these addresses to examine the objects contained within the array (edited for brevity):

0:000> !do 00ac2a1c
Name: System.String
MethodTable: 5b99452c
EEClass: 5b9d7f10
Size: 34(0x22) bytes
(C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
String: String 1


0:000> !do 00ac2a40
Name: System.String
MethodTable: 5b99452c
EEClass: 5b9d7f10
Size: 34(0x22) bytes
(C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
String: String 2


0:000> !do 00ac2a64
Name: System.String
MethodTable: 5b99452c
EEClass: 5b9d7f10
Size: 34(0x22) bytes
(C:\WINNT\Microsoft.NET\Framework\v2.0.40428\assembly\GAC_32\mscorlib\2.0.3600.0__b77a5c561934e089\mscorlib.dll)
String: String 3

From looking at this data, we can see that the three strings are "String 1", "String 2", "String 3".