Hypothetical - about making a header for an *existing* static/dynamic library - c++

I want to learn more about unix/linux and this question popped into my head - let's say I made a static/dynamic library (.a or .so) and lost the c/c++ source code and header file. Default nm output gives me the names of the symbols but I need to know return types and parameter count/types to make a header. Is it possible to get this extra information somehow to reverse engineer a header for a given library?

You tagged C and C++ and the answer varies slightly between the two.
For C++, the method names of classes have type information embedded in the symbol name. You just have to figure how what kind of name mangling the compiler that compiled the library did.
For C, there's no real clean way to do it. You could take apart the assembly and analyze which registers and stack areas are read without having been written to figure out how many parameters a function takes. This would require knowledge of the calling conventions used by whatever compiler compiled the library.
Similarly, you can look at how each parameters is used in the assembly. If you see it being used in a load instruction, it is most likely a pointer of some sort while if you see it being used in arithmetic, it's possibly an integer of some sort.
For the return type, you can check if anything seemingly meaningful is placed in the return register before a return instruction. Again, this requires knowledge of calling conventions for your platform.
Here's an example of how I would do things in ARM assembly.
I know that parameters in ARM are passed in registers r0 to r3 and the return value is stored in register r0. With that in mind, we can begin reverse engineering. Let's take a look at the assembly for two functions and try to work out what the function prototype was.
00000000 <func1>:
0: e3510000 cmp r1, #0
4: 0a000007 beq 28 <func1+0x28>
8: e0801001 add r1, r0, r1
c: e1a03000 mov r3, r0
10: e3a00000 mov r0, #0
14: e4d32001 ldrb r2, [r3], #1
18: e1530001 cmp r3, r1
1c: e0800002 add r0, r0, r2
20: 1afffffb bne 14 <func1+0x14>
24: e12fff1e bx lr
28: e1a00001 mov r0, r1
2c: e12fff1e bx lr
If we take a look here, r0 and r1 are both read before anything was written to it. We can also see r2 and r3 are written to before they were read. We can therefore infer that func1 has a maximum of two paramaters.
We also realise that r0 is moved to r3 and then used as an address to ldrb, which is an instruction to load a byte from memory. Hence, we infer that the first parameter is a pointer. Because the instruction only loads a single byte, we can also tell it might be a pointer to some sort of one byte data type.
The second parameter in r1 never seems to be used except in compare and add instructions so it is possibly an integer.
Before each bx lr (a return-to-caller instruction), something is placed in r0 so we infer that the function returns some sort of value.
If this function were presented to me, I'd guess that the function prototype would look something like this:
int func1(unsigned char *, int);
Original:
unsigned int func1(void *, unsigned int);
Here's an another function
00000030 <func2>:
30: e0822001 add r2, r2, r1
34: e5c02000 strb r2, [r0]
38: e12fff1e bx lr
This one is very easy.
We see that r0, r1 and r2 are all read from before being written to so we can guess that the function takes three parameters. r0 is used as an address to a strb instruction (store byte) so it is probably a pointer. Again, it only stores a byte so it is probably a pointer to a byte sized data type.
The other two are only used in an add instruction so are probably integers.
Nothing seems to be placed into r0 at the end so the function either returns the first parameter or doesn't return a value.
I would guess the prototype would be one of the following
void func2(unsigned char *, int, int);
unsigned char *func2(unsigned char *, int, int);
Original:
void func2(char *, char, char);

Keeping in mind that caller/callee conventions vary for different processor instruction sets and you are already aware of name mangling while using c and c++ libraries together, you can try the following way:
gdb <executable>
....
disas <function name>
....
Here you can make a wild guess about the type of return value and parameters using the bit size of those values written on stack making use of assembly code.

Related

How does the following C ++ function pass parameters to the following arm assembly function? [duplicate]

It's been a while since I last coded arm assembler and I'm a little rusty on the details. If I call a C function from arm, I only have to worry about saving r0-r3 and lr, right?
If the C function uses any other registers, is it responsible for saving those on the stack and restoring them? In other words, the compiler would generate code to do this for C functions.
For example if I use r10 in an assembler function, I don't have to push its value on the stack, or to memory, and pop/restore it after a C call, do I?
This is for arm-eabi-gcc 4.3.0.
It depends on the ABI for the platform you are compiling for. On Linux, there are two ARM ABIs; the old one and the new one. AFAIK, the new one (EABI) is in fact ARM's AAPCS. The complete EABI definitions currently live here on ARM's infocenter.
From the AAPCS, §5.1.1:
r0-r3 are the argument and scratch registers; r0-r1 are also the result registers
r4-r8 are callee-save registers
r9 might be a callee-save register or not (on some variants of AAPCS it is a special register)
r10-r11 are callee-save registers
r12-r15 are special registers
A callee-save register must be saved by the callee (in opposition to a caller-save register, where the caller saves the register); so, if this is the ABI you are using, you do not have to save r10 before calling another function (the other function is responsible for saving it).
Edit: Which compiler you are using makes no difference; gcc in particular can be configured for several different ABIs, and it can even be changed on the command line. Looking at the prologue/epilogue code it generates is not that useful, since it is tailored for each function and the compiler can use other ways of saving a register (for instance, saving it in the middle of a function).
Terminology: "callee-save" is a synonym for "non-volatile" or "call-preserved": What are callee and caller saved registers?
When making a function call, you can assume that the values in r4-r11 (except maybe r9) are still there after (call-preserved), but not for r0-r3 (call-clobbered / volatile).
32-bit ARM calling conventions are specified by AAPCSFrom the AAPCS, §5.1.1 Core registers:
r0-r3 are the argument and scratch registers; r0-r1 are also the result registers
r4-r8 are callee-save registers
r9 might be a callee-save register or not (on some variants of AAPCS it is a special register)
r10-r11 are callee-save registers
r12-r15 are special registers
From the AAPCS, §5.1.2.1 VFP register usage conventions:
s16–s31 (d8–d15, q4–q7) must be preserved
s0–s15 (d0–d7, q0–q3) and d16–d31 (q8–q15) do not need to be preserved
Original post:
arm-to-c-calling-convention-neon-registers-to-save
64-bit ARM calling conventions are specified by AAPCS64General-purpose Registers section specifies what registers need be preserved.
r0-r7 are parameter/result registers
r9-r15 are temporary registers
r19-r28 are callee-saved registers.
All others (r8, r16-r18, r29, r30, SP) have special meaning and some might be treated as temporary registers.
SIMD and Floating-Point Registers specifies Neon and floating point registers.
For 64-bit ARM, A64 (from Procedure Call Standard for the ARM 64-bit Architecture)
There are thirty-one, 64-bit, general-purpose (integer) registers visible to the A64 instruction set; these are labeled r0-r30. In a 64-bit context these registers are normally referred to using the names x0-x30; in a 32-bit context the registers are specified by using w0-w30. Additionally, a stack-pointer register, SP, can be used with a restricted number of instructions.
SP The Stack Pointer
r30 LR The Link Register
r29 FP The Frame Pointer
r19…r28 Callee-saved registers
r18 The Platform Register, if needed; otherwise a temporary register.
r17 IP1 The second intra-procedure-call temporary register (can be used
by call veneers and PLT code); at other times may be used as a
temporary register.
r16 IP0 The first intra-procedure-call scratch register (can be used by call
veneers and PLT code); at other times may be used as a
temporary register.
r9…r15 Temporary registers
r8 Indirect result location register
r0…r7 Parameter/result registers
The first eight registers, r0-r7, are used to pass argument values into a subroutine and to return result values from a function. They may also be used to hold intermediate values within a routine (but, in general, only between subroutine calls).
Registers r16 (IP0) and r17 (IP1) may be used by a linker as a scratch register between a routine and any subroutine it calls. They can also be used within a routine to hold intermediate values between subroutine calls.
The role of register r18 is platform specific. If a platform ABI has need of a dedicated general purpose register to carry inter-procedural state (for example, the thread context) then it should use this register for that purpose. If the platform ABI has no such requirements, then it should use r18 as an additional temporary register. The platform ABI specification must document the usage for this register.
SIMD
The ARM 64-bit architecture also has a further thirty-two registers, v0-v31, which can be used by SIMD and Floating-Point operations. The precise name of the register will change indicating the size of the access.
Note: Unlike in AArch32, in AArch64 the 128-bit and 64-bit views of a SIMD and Floating-Point register do not overlap multiple registers in a narrower view, so q1, d1 and s1 all refer to the same entry in the register bank.
The first eight registers, v0-v7, are used to pass argument values into a subroutine and to return result values from a function. They may also be used to hold intermediate values within a routine (but, in general, only between subroutine calls).
Registers v8-v15 must be preserved by a callee across subroutine calls; the remaining registers (v0-v7, v16-v31) do not need to be preserved (or should be preserved by the caller). Additionally, only the bottom 64-bits of each value stored in v8-v15 need to be preserved; it is the responsibility of the caller to preserve larger values.
The answers of CesarB and Pavel provided quotes from AAPCS, but open issues remain. Does the callee save r9? What about r12? What about r14? Furthermore, the answers were very general, and not specific to the arm-eabi toolchain as requested. Here's a practical approach to find out which register are callee-saved and which are not.
The following C code contain an inline assembly block, that claims to modify registers r0-r12 and r14. The compiler will generate the code to save the registers required by the ABI.
void foo() {
asm volatile ( "nop" : : : "r0", "r1", "r2", "r3", "r4", "r5", "r6", "r7", "r8", "r9", "r10", "r11", "r12", "r14");
}
Use the command line arm-eabi-gcc-4.7 -O2 -S -o - foo.c
and add the switches for your platform (such as -mcpu=arm7tdmi for example).
The command will print the generated assembly code on STDOUT. It may look something like this:
foo:
stmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}
nop
ldmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}
bx lr
Note, that the compiler generated code saves and restores r4-r11. The compiler does not save r0-r3, r12. That it restores r14 (alias lr) is purely accidental as I know from experience that the exit code may also load the saved lr into r0 and then do a "bx r0" instead of "bx lr". Either by adding the -mcpu=arm7tdmi -mno-thumb-interwork or by using -mcpu=cortex-m4 -mthumb we obtain slightly different assembly code that looks like this:
foo:
stmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, lr}
nop
ldmfd sp!, {r4, r5, r6, r7, r8, r9, sl, fp, pc}
Again, r4-r11 are saved and restored. But r14 (alias lr) is not restored.
To summarize:
r0-r3 are not callee-saved
r4-r11 are callee-saved
r12 (alias ip) is not callee-saved
r13 (alias sp) is callee-saved
r14 (alias lr) is not callee-saved
r15 (alias pc) is the program counter and is set to the value of lr prior to the function call
This holds at least for arm-eabi-gcc's default's. There are command line switches (in particular the -mabi switch) that may influence the results.
According ARM's aapcs32 and aapcs64, finally summarized to this:
online view
There is also difference at least at Cortex M3 architecture for function call and interrupt.
If an Interrupt occurs it will make automatic push R0-R3,R12,LR,PC onto Stack and when return form IRQ automatic POP. If you use other registers in IRQ routine you have to push/pop them onto Stack manually.
I don't think this automatic PUSH and POP is made for a Function call (jump instruction). If convention says R0-R3 can be used only as an argument, result or scratch registers, so there is no need to store them before function call because there shouldn't be any value used later after function return. But same as in an interrupt you have to store all other CPU registers if you use them in your function.

How are assembly directives instructed?

To elaborate the question on the title, suppose I declared the following array in C++,
int myarr[10];
This disassembles to the following in x86
myarr:
.zero 40
Now, AFAIK this .zero directive is used for convention and is not an instruction. Then, how exactly is this directive translated to x86(or any other architecture, it's not the emphasis here) instructions? Because, for all we know the CPU can only execute instructions. So I guess these directives are somehow translated to instructions, am I correct?
I could generalize the question by also asking how .word .long etc. are translated into instructions, but I think it is clear.
The output of the assembler is an object module. In the object module are representations of various sections for a program. Each section has a size, some attributes, and possibly some data to be put into the section.
For example, a section may be a few thousand bytes, have attributes indicating it contains instructions for execution, and have data that consists of those instructions. Another section might be several hundred bytes but have no data—it is just space to be allocated when the program starts. Another section might be very big and have non-zero data that contains its initial values when the program starts.
To assemble a .zero 40 directive, the compiler just includes forty bytes of zeros in the section it is currently building. When it writes the final output, it will include those zeros in that section. Data directives like this and .word and such simply tell the assembler what data to put into its output.
unsigned int stuff[10];
void fun ( void )
{
unsigned int r;
for(r=0;r<10;r++) stuff[r]=r;
}
using ARM...
00000000 <fun>:
0: e3a03000 mov r3, #0
4: e59f2010 ldr r2, [pc, #16] ; 1c <fun+0x1c>
8: e5a23004 str r3, [r2, #4]!
c: e2833001 add r3, r3, #1
10: e353000a cmp r3, #10
14: 1afffffb bne 8 <fun+0x8>
18: e12fff1e bx lr
1c: 00000ffc
Disassembly of section .bss:
00001000 <stuff>:
...
The array stuff is simply data it is not code it is not instructions and won't be, the directive in question you asked about won't become code, it cants it is data.
If you want to see code, instructions, then you need to put lines of high level language that act on data for example as shown here. And in that case the compiler generates code.
Looking at this compilers actual output (comments and other non-essentials removed)
fun:
mov r3, #0
ldr r2, .L6
.L2:
str r3, [r2, #4]!
add r3, r3, #1
cmp r3, #10
bne .L2
bx lr
.L7:
.align 2
.L6:
.word stuff-4
...
.comm stuff,40,4
the .comm in this case is how they declared the data that represents the array in the high level language. and the other stuff is mostly code. the .align is there so that the address of L6 is aligned so that you don't get an alignment fault when you try to read it.
.word is a directive, what you see here is .text vs .data while it is just one simple C program with the array and the code right there next to each other. because code can possibly live in read only memory like flash and data needs to be in read/write memory and at compile time the compiler doesn't know where the data is relative to the code, so it generates an abstraction by placing a read only word in the code that the linker fills in later, the code is generic and whatever the linker puts in there it uses. The linker "places" .text and .bss in this case it wasn't initialized so it isn't actually .data and then makes that connection in the code.
labels are directives if you will so that the programmer or code generator (compiler) doesn't have to count instructions or overall size of instructions to make relative jumps. Let the tools do that for you.
1c: 00000ffc
Disassembly of section .bss:
00001000 <stuff>:
...
and based on the way I linked this (non actually a working) program stuff is the only data item in this program and the linker placed it where I asked at address 0x1000, then went back and filled in that .word directive to be stuff-4 which is 0xFFC so that the code as compiled works.
directives are not part of the instruction set but are part of the assembly language, note that assembly language is defined by the assembler, the tool, not the instruction set/target. There are countless different x86 assembly languages and AT&T vs Intel is not the primary difference, the directives how you define a label, how you indicate a number is hex or decimal, because of the vagueness of the instructions as defined in the early docs lots of adjectives if you will to be able to specify which mov instruction you were actually after and even though that's part of the instruction and not a directive those adjectives varied across assembly languages. ARM, MIPS, and many if not most others have had tools created with incompatible assembly languages. .zero for example being one of those incompatible things.
In any case an assembly language in question needs to be able to define data and then have a way for code to reference that data in order to make useful programs.
The notion of a one to one line of assembly language to instructions is very misleading and don't get fooled by it, today's compilers generate almost as much non-code as code in their output. Lots of directives and other information.

Why does ARM use two instructions to mask a value?

For the following function...
uint16_t swap(const uint16_t value)
{
return value << 8 | value >> 8;
}
...why does ARM gcc 6.3.0 with -O2 yield the following assembly?
swap(unsigned short):
lsr r3, r0, #8
orr r0, r3, r0, lsl #8
lsl r0, r0, #16 # shift left
lsr r0, r0, #16 # shift right
bx lr
It appears the compiler is using two shifts to mask off the unwanted bytes, instead of using a logical AND. Could the compiler instead use and r0, r0, #4294901760?
Older ARM assembly cannot create constants easily. Instead, they are loaded into literal pools and then read in via a memory load. This and you suggest can only take I believe an 8-bit literal with shift. Your 0xFFFF0000 requires 16-bits to do as 1 instructions.
So, we can load from memory and do an and (slow),
Take 2 instructions to create the value and 1 to and (longer),
or just shift twice cheaply and call it good.
The compiler chose the shifts and honestly, it is plenty fast.
Now for a reality check:
Worrying about a single shift, unless this is a 100% for sure bottleneck is a waste of time. Even if the compiler was sub-optimal, you will almost never feel it. Worry about "hot" loops in code instead for micro-ops like this. Looking at this from curiosity is awesome. Worrying about this exact code for performance in your app, not so much.
Edit:
It has been noted by others here that newer versions of the ARM specifications allow this sort of thing to be done more efficiently. This shows that it is important, when talking at this level, to specify the Chip or at least the exact ARM spec we are dealing with. I was assuming ancient ARM from the lack of "newer" instructions given from your output. If we are tracking compiler bugs, then this assumption may not hold and knowing the specification is even more important. For a swap like this, there are indeed simpler instructions to handle this in later versions.
Edit 2
One thing that could be done to possibly make this faster is to make it inline'd. In that case, the compiler could interleave these operations with other work. Depending on the CPU, this could double the throughput here as many ARM CPUs have 2 integer instruction pipelines. Spread out the instructions enough so that there are no hazards, and away it goes. This has to be weighed against I-Cache usage, but in a case where it mattered, you could see something better.
There is a missed-optimization here, but and isn't the missing piece. Generating a 16-bit constant isn't cheap. For a loop, yes it would be a win to generate a constant outside the loop and use just and inside the loop. (TODO: call swap in a loop over an array and see what kind of code we get.)
For an out-of-order CPU, it could also be worth using multiple instructions off the critical path to build a constant, then you only have one AND on the critical path instead of two shifts. But that's probably rare, and not what gcc chooses.
AFAICT (from looking at compiler output for simple functions), the ARM calling convention guarantees there's no high garbage in input registers, and doesn't allow leaving high garbage in return values. i.e. on input, it can assume that the upper 16 bits of r0 are all zero, but must leave them zero on return. The value << 8 left shift is thus a problem, but the value >> 8 isn't (it doesn't have to worry about shifting garbage down into the low 16).
(Note that x86 calling conventions aren't like this: return values are allowed to have high garbage. (Maybe because the caller can simply use the 16-bit or 8-bit partial register). So are input values, except as an undocumented part of the x86-64 System V ABI: clang depends on input values being sign/zero extended to 32-bit. GCC provides this when calling, but doesn't assume as a callee.)
ARMv6 has a rev16 instruction which byte-swaps the two 16-bit halves of a register. If the upper 16 bits are already zeroed, they don't need to be re-zeroed, so gcc -march=armv6 should compile the function to just rev16. But in fact it emits a uxth to extract and zero-extend the low half-word. (i.e. exactly the same thing as and with 0x0000FFFF, but without needing a large constant). I believe this is pure missed optimization; presumably gcc's rotate idiom, or its internal definition for using rev16 that way, doesn't include enough info to let it realize the top half stays zeroed.
swap: ## gcc6.3 -O3 -march=armv6 -marm
rev16 r0, r0
uxth r0, r0 # not needed
bx lr
For ARM pre v6, a shorter sequence is possible. GCC only finds it if we hand-hold it towards the asm we want:
// better on pre-v6, worse on ARMv6 (defeats rev16 optimization)
uint16_t swap_prev6(const uint16_t value)
{
uint32_t high = value;
high <<= 24; // knock off the high bits
high >>= 16; // and place the low8 where we want it
uint8_t low = value >> 8;
return high | low;
//return value << 8 | value >> 8;
}
swap_prev6: # gcc6.3 -O3 -marm. (Or armv7 -mthumb for thumb2)
lsl r3, r0, #24
lsr r3, r3, #16
orr r0, r3, r0, lsr #8
bx lr
But this defeats the gcc's rotate-idiom recognition, so it compiles to this same code even with -march=armv6 when the simple version compiles to rev16 / uxth.
All source + asm on the Godbolt compiler explorer
ARM is a RISC machine (Advanced RISC Machine), and thus, all instrutcions are encoded in the same size, capping at 32bit.
Immediate values in instructions are assigned to a certain number of bits, and AND instruction simply doesn't have enought bits assigned to immediate values to express any 16bit value.
That's the reason for the compiler resorting to two shift instructions instead.
However, if your target CPU is ARMv6 (ARM11) or higher, the compiler takes leverage from the new REV16 instruction, and then masks the lower 16bit by UXTH instruction which is unnecessary and stupid, but there is simply no conventional way to persuade the compiler not to do this.
If you think that you would be served well by GCC intrinsic __builtin_bswap16, you are dead wrong.
uint16_t swap(const uint16_t value)
{
return __builtin_bswap16(value);
}
The function above generates exactly the same machine code that your original C code did.
Even using inline assembly doesn't help either
uint16_t swap(const uint16_t value)
{
uint16_t result;
__asm__ __volatile__ ("rev16 %[out], %[in]" : [out] "=r" (result) : [in] "r" (value));
return result;
}
Again, exactly the same. You cannot get rid of the pesky UXTH as long as you use GCC; It simply cannot read from the context that the upper 16bits are all zeros to start with and thus, UXTH is unnecessary.
Write the whole function in assembly; That's the only option.
This is the optimal solution, the AND would require at least two more instructions possibly having to stop and wait for a load to happen of the value to mask. So worse in a couple of ways.
00000000 <swap>:
0: e1a03420 lsr r3, r0, #8
4: e1830400 orr r0, r3, r0, lsl #8
8: e1a00800 lsl r0, r0, #16
c: e1a00820 lsr r0, r0, #16
10: e12fff1e bx lr
00000000 <swap>:
0: ba40 rev16 r0, r0
2: b280 uxth r0, r0
4: 4770 bx lr
The latter is armv7 but at the same time it is because they added instructions to support this kind of work.
Fixed length RISC instructions have by definition a problem with constants. MIPS chose one way, ARM chose another. Constants are a problem on CISC as well just a different problem. Not difficult to create something that takes advantage of ARMS barrel shifter and shows a disadvantage of MIPS solution and vice versa.
The solution actually has a bit of elegance to it.
Part of this as well is the overall design of the target.
unsigned short fun ( unsigned short x )
{
return(x+1);
}
0000000000000010 <fun>:
10: 8d 47 01 lea 0x1(%rdi),%eax
13: c3 retq
gcc chooses not to return the 16 bit variable you asked for it returns a 32 bit, it doesnt properly/correctly implement the function I asked for with my code. But that is okay if when the user of the data gets that result or uses it the mask happens there or with this architecture ax is used instead of eax. for example.
unsigned short fun ( unsigned short x )
{
return(x+1);
}
unsigned int fun2 ( unsigned short x )
{
return(fun(x));
}
0000000000000010 <fun>:
10: 8d 47 01 lea 0x1(%rdi),%eax
13: c3 retq
0000000000000020 <fun2>:
20: 8d 47 01 lea 0x1(%rdi),%eax
23: 0f b7 c0 movzwl %ax,%eax
26: c3 retq
A compiler design choice (likely based on architecture) not an implementation bug.
Note that for a sufficiently sized project, it is easy to find missed optimization opportunities. No reason to expect an optimizer to be perfect (it isnt and cant be). They just need to be more efficient than a human doing it by hand for that sized project on average.
This is why it is commonly said that for performance tuning you dont pre-optimize or just jump to asm immediately you use the high level language and the compiler you in some way profile your way through to find the performance problems then hand code those, why hand code them because we know we can at times out perform the compiler, implying the compiler output can be improved upon.
This isnt a missed optimization opportunity, this is instead a very elegant solution for the instruction set. Masking a byte is simpler
unsigned char fun ( unsigned char x )
{
return((x<<4)|(x>>4));
}
00000000 <fun>:
0: e1a03220 lsr r3, r0, #4
4: e1830200 orr r0, r3, r0, lsl #4
8: e20000ff and r0, r0, #255 ; 0xff
c: e12fff1e bx lr
00000000 <fun>:
0: e1a03220 lsr r3, r0, #4
4: e1830200 orr r0, r3, r0, lsl #4
8: e6ef0070 uxtb r0, r0
c: e12fff1e bx lr
the latter being armv7, but with armv7 they recognized and solved these issues you cant expect the programmer to always use natural sized variables, some feel the need to use less optimal sized variables. sometimes you still have to mask to a certain size.

Instruction disassembly for ARM

I just setup a raspberry pi machine and tried reverse engineering the following piece of code.
#include<stdio.h>
int main() {
printf("this is a test\n");
}
For the most part the following disassembly in gdb seemed to make sense.
0x000083c8 <+0>: push {r11, lr}
0x000083cc <+4>: add r11, sp, #4
0x000083d0 <+8>: ldr r0, [pc, #8] ; 0x83e0 <main+24>
0x000083d4 <+12>: bl 0x82ec <puts>
0x000083d8 <+16>: mov r0, r3
0x000083dc <+20>: pop {r11, pc}
0x000083e0 <+24>: andeq r8, r0, r4, asr r4
However, I fail to understand why the instruction at 0x000083e0 exists. Is that instruction even a part of the main function? Wouldn't the value that was pushed in at 0x000083c8 be popped out into pc, immediately transferring control over to some other location?
Also I tried setting a breakpoint at 0x000083e0 -- I seem to be getting a very strange SEGFAULT. Why would that be?
When this function is called (i.e. when execution begins at instruction 0x000083c8), the link register (LR) should already contain the return address. Fast-forward to 0x000083d8: the puts function's return result is placed in R0 in accordance with the ARM C calling convention (link, link). Then, the return address is popped from the stack into the PC - effectively ending execution of this function. This implies that the instruction at 0x000083e0 is not a part of your program, and your inspection should be limited to instructions 0x000083c8 through 0x000083dc.
So to answer your questions:
Correct.
The "instruction" at 0x000083e0 is essentially junk. You may not even have execution and/or access privileges to this memory depending on the specifics of your ARM core (Does it have an MMU, etc?). Thus, a seg fault is a reasonable outcome when attempting to inspect that location.
EDIT: in agreement with comments below, the contents of 0x000083e0 should be interpreted as data, not instructions.
Four bytes at 0x000083e0 isn't junk. It is part of the PC relative load at
0x000083d0 <+8>: ldr r0, [pc, #8] ; 0x83e0 <main+24>
It is also visible in the comment as ; 0x83e0 <main+24>.
Problem here since you need to pass address of a string to puts, whose address might change during linking step, compiler needs to create suitable code for such further processing. Thus address of string ends up in instruction stream yet outside of any execution context.

Strange behaviour of ldr [pc, #value]

I was debugging some c++ code (WinCE 6 on ARM platform),
and i find some behavior strange:
4277220C mov r3, #0x93, 30
42772210 str r3, [sp]
42772214 ldr r3, [pc, #0x69C]
42772218 ldr r2, [pc, #0x694]
4277221C mov r1, #0
42772220 ldr r0, [pc, #0x688]
Line 42772214 ldr r3, [pc, #0x69C] is used to get some constant from .DATA section, at least I think so.
What is strange that according to the code r2 should be filled with memory from address pc=0x42772214 + 0x69C = 0x427728B0, but according to the memory contents it's loaded from 0x427728B8 (8bytes+), it happens for other ldr usages too.
Is it fault of the debugger or my understanding of ldr/pc?
Another issue I don't get - why access to the .data section is relative to the executed code? I find it little bit strange.
And one more issue: i cannot find syntax of the 1st mov command (any one could point me a optype specification for the Thumb (1C2))
Sorry for the laic description, but I'm just familiarizing with the assemblies.
This is correct. When pc is used for reading there is an 8-byte offset in ARM mode and 4-byte offset in Thumb mode.
From the ARM-ARM:
When an instruction reads the PC, the value read depends on which instruction set it comes from:
For an ARM instruction, the value read is the address of the instruction plus 8 bytes. Bits [1:0] of this value are always zero, because ARM instructions are always word-aligned.
For a Thumb instruction, the value read is the address of the instruction plus 4 bytes. Bit [0] of this value is always zero, because Thumb instructions are always halfword-aligned.
This way of reading the PC is primarily used for quick, position-independent addressing of nearby instructions and data, including position-independent branching within a program.
There are 2 reasons for pc-relative addressing.
Position-independent code, which is in your case.
Get some complicated constants nearby which cannot be written in 1 simple instruction, e.g. mov r3, #0x12345678 is impossible to complete in 1 instruction, so the compiler may put this constant in the end of the function and use e.g. ldr r3, [pc, #0x50] to load it instead.
I don't know what mov r3, #0x93, 30 means. Probably it is mov r3, #0x93, rol 30 (which gives 0xC0000024)?